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

OZ audit fixes - Graph Horizon #1125

Open
wants to merge 8 commits into
base: horizon
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -189,12 +189,14 @@ interface IHorizonStakingMain {
* @param verifier The address of the verifier
* @param delegator The address of the delegator
* @param tokens The amount of tokens undelegated
* @param tokens The amount of shares undelegated
*/
event TokensUndelegated(
address indexed serviceProvider,
address indexed verifier,
address indexed delegator,
uint256 tokens
uint256 tokens,
uint256 shares
);

/**
Expand Down Expand Up @@ -254,6 +256,7 @@ interface IHorizonStakingMain {
* @param shares The amount of shares being thawed
* @param thawingUntil The timestamp until the stake is thawed
* @param thawRequestId The ID of the thaw request
* @param nonce The nonce of the thaw request
*/
event ThawRequestCreated(
IHorizonStakingTypes.ThawRequestType indexed requestType,
Expand All @@ -262,7 +265,8 @@ interface IHorizonStakingMain {
address owner,
uint256 shares,
uint64 thawingUntil,
bytes32 thawRequestId
bytes32 thawRequestId,
uint256 nonce
);

/**
Expand Down Expand Up @@ -508,6 +512,11 @@ interface IHorizonStakingMain {
*/
error HorizonStakingLegacySlashFailed();

/**
* @notice Thrown when there attempting to slash a provision with no tokens to slash.
*/
error HorizonStakingNoTokensToSlash();

// -- Functions --

/**
Expand Down Expand Up @@ -657,7 +666,8 @@ interface IHorizonStakingMain {
/**
* @notice Remove tokens from a provision and move them back to the service provider's idle stake.
* @dev The parameter `nThawRequests` can be set to a non zero value to fulfill a specific number of thaw
* requests in the event that fulfilling all of them results in a gas limit error.
* requests in the event that fulfilling all of them results in a gas limit error. Otherwise, the function
* will attempt to fulfill all thaw requests until the first one that is not yet expired is found.
*
* Requirements:
* - Must have previously initiated a thaw request using {thaw}.
Expand Down Expand Up @@ -781,7 +791,8 @@ interface IHorizonStakingMain {
/**
* @notice Withdraw undelegated tokens from a provision after thawing.
* @dev The parameter `nThawRequests` can be set to a non zero value to fulfill a specific number of thaw
* requests in the event that fulfilling all of them results in a gas limit error.
* requests in the event that fulfilling all of them results in a gas limit error. Otherwise, the function
* will attempt to fulfill all thaw requests until the first one that is not yet expired is found.
* @dev If the delegation pool was completely slashed before withdrawing, calling this function will fulfill
* the thaw requests with an amount equal to zero.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ interface IHorizonStakingTypes {
* @param createdAt Timestamp when the provision was created
* @param maxVerifierCutPending Pending value for `maxVerifierCut`. Verifier needs to accept it before it becomes active.
* @param thawingPeriodPending Pending value for `thawingPeriod`. Verifier needs to accept it before it becomes active.
* @param lastParametersStagedAt Timestamp when the provision parameters were last staged. Can be used by data service implementation to
* implement arbitrary parameter update logic.
* @param thawingNonce Value of the current thawing nonce. Thaw requests with older nonces are invalid.
*/
struct Provision {
Expand All @@ -33,6 +35,7 @@ interface IHorizonStakingTypes {
uint64 createdAt;
uint32 maxVerifierCutPending;
uint64 thawingPeriodPending;
uint256 lastParametersStagedAt;
uint256 thawingNonce;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ contract GraphTallyCollector is EIP712, GraphDirectory, Authorizable, IGraphTall
/// @notice The EIP712 typehash for the ReceiptAggregateVoucher struct
bytes32 private constant EIP712_RAV_TYPEHASH =
keccak256(
"ReceiptAggregateVoucher(address payer,address serviceProvider,address dataService,uint64 timestampNs,uint128 valueAggregate,bytes metadata)"
"ReceiptAggregateVoucher(bytes32 collectionId,address payer,address serviceProvider,address dataService,uint64 timestampNs,uint128 valueAggregate,bytes metadata)"
);

/// @notice Tracks the amount of tokens already collected by a data service from a payer to a receiver.
Expand Down Expand Up @@ -189,6 +189,7 @@ contract GraphTallyCollector is EIP712, GraphDirectory, Authorizable, IGraphTall
keccak256(
abi.encode(
EIP712_RAV_TYPEHASH,
_rav.collectionId,
_rav.payer,
_rav.serviceProvider,
_rav.dataService,
Expand Down
19 changes: 14 additions & 5 deletions packages/horizon/contracts/staking/HorizonStaking.sol
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain {
if ((prov.maxVerifierCutPending != newMaxVerifierCut) || (prov.thawingPeriodPending != newThawingPeriod)) {
prov.maxVerifierCutPending = newMaxVerifierCut;
prov.thawingPeriodPending = newThawingPeriod;
prov.lastParametersStagedAt = block.timestamp;
emit ProvisionParametersStaged(serviceProvider, verifier, newMaxVerifierCut, newThawingPeriod);
}
}
Expand Down Expand Up @@ -410,7 +411,7 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain {
Provision storage prov = _provisions[serviceProvider][verifier];
DelegationPoolInternal storage pool = _getDelegationPool(serviceProvider, verifier);
uint256 tokensProvisionTotal = prov.tokens + pool.tokens;
require(tokensProvisionTotal != 0, HorizonStakingInsufficientTokens(tokensProvisionTotal, tokens));
require(tokensProvisionTotal != 0, HorizonStakingNoTokensToSlash());

uint256 tokensToSlash = MathUtils.min(tokens, tokensProvisionTotal);

Expand Down Expand Up @@ -716,6 +717,7 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain {
createdAt: uint64(block.timestamp),
maxVerifierCutPending: _maxVerifierCut,
thawingPeriodPending: _thawingPeriod,
lastParametersStagedAt: 0,
thawingNonce: 0
});

Expand Down Expand Up @@ -798,7 +800,8 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain {
/**
* @notice Remove tokens from a provision and move them back to the service provider's idle stake.
* @dev The parameter `nThawRequests` can be set to a non zero value to fulfill a specific number of thaw
* requests in the event that fulfilling all of them results in a gas limit error.
* requests in the event that fulfilling all of them results in a gas limit error. Otherwise, the function
* will attempt to fulfill all thaw requests until the first one that is not yet expired is found.
* @param _serviceProvider The service provider address
* @param _verifier The verifier address
* @param _nThawRequests The number of thaw requests to fulfill. Set to 0 to fulfill all thaw requests.
Expand Down Expand Up @@ -947,14 +950,15 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain {
pool.thawingNonce
);

emit TokensUndelegated(_serviceProvider, _verifier, msg.sender, tokens);
emit TokensUndelegated(_serviceProvider, _verifier, msg.sender, tokens, _shares);
return thawRequestId;
}

/**
* @notice Withdraw undelegated tokens from a provision after thawing.
* @dev The parameter `nThawRequests` can be set to a non zero value to fulfill a specific number of thaw
* requests in the event that fulfilling all of them results in a gas limit error.
* requests in the event that fulfilling all of them results in a gas limit error. Otherwise, the function
* will attempt to fulfill all thaw requests until the first one that is not yet expired is found.
* @dev If the delegation pool was completely slashed before withdrawing, calling this function will fulfill
* the thaw requests with an amount equal to zero.
* @param _requestType The type of thaw request (Provision or Delegation).
Expand Down Expand Up @@ -1063,13 +1067,17 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain {
_owner,
_shares,
_thawingUntil,
thawRequestId
thawRequestId,
_thawingNonce
);
return thawRequestId;
}

/**
* @notice Traverses a thaw request list and fulfills expired thaw requests.
* @dev Note that the list is traversed by creation date not by thawing until date. Traversing will stop
* when the first thaw request that is not yet expired is found even if later thaw requests have expired. This
* could happen for example when the thawing period is shortened.
* @param _params The parameters for fulfilling thaw requests
* @return The amount of thawed tokens
* @return The amount of tokens still thawing
Expand Down Expand Up @@ -1171,6 +1179,7 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain {
uint256 tokens = 0;
bool validThawRequest = thawRequest.thawingNonce == thawingNonce;
if (validThawRequest) {
// sharesThawing cannot be zero if there is a valid thaw request so the next division is safe
tokens = (thawRequest.shares * tokensThawing) / sharesThawing;
tokensThawing = tokensThawing - tokens;
sharesThawing = sharesThawing - thawRequest.shares;
Expand Down
18 changes: 11 additions & 7 deletions packages/horizon/contracts/staking/HorizonStakingBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -182,14 +182,18 @@ abstract contract HorizonStakingBase is
bytes32 thawRequestId = thawRequestList.head;
while (thawRequestId != bytes32(0)) {
ThawRequest storage thawRequest = _getThawRequest(requestType, thawRequestId);
if (thawRequest.thawingUntil <= block.timestamp) {
uint256 tokens = (thawRequest.shares * tokensThawing) / sharesThawing;
tokensThawing = tokensThawing - tokens;
sharesThawing = sharesThawing - thawRequest.shares;
thawedTokens = thawedTokens + tokens;
} else {
break;
if (thawRequest.thawingNonce == prov.thawingNonce) {
if (thawRequest.thawingUntil <= block.timestamp) {
// sharesThawing cannot be zero if there is a valid thaw request so the next division is safe
uint256 tokens = (thawRequest.shares * tokensThawing) / sharesThawing;
tokensThawing = tokensThawing - tokens;
sharesThawing = sharesThawing - thawRequest.shares;
thawedTokens = thawedTokens + tokens;
} else {
break;
}
}

thawRequestId = thawRequest.next;
}
return thawedTokens;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,7 @@ abstract contract HorizonStakingSharedTest is GraphBaseTest {
assertEq(afterProvision.createdAt, uint64(block.timestamp));
assertEq(afterProvision.maxVerifierCutPending, maxVerifierCut);
assertEq(afterProvision.thawingPeriodPending, thawingPeriod);
assertEq(afterProvision.lastParametersStagedAt, 0);
assertEq(afterProvision.thawingNonce, 0);
assertEq(afterServiceProvider.tokensStaked, beforeServiceProvider.tokensStaked);
assertEq(afterServiceProvider.tokensProvisioned, tokens + beforeServiceProvider.tokensProvisioned);
Expand Down Expand Up @@ -387,6 +388,7 @@ abstract contract HorizonStakingSharedTest is GraphBaseTest {
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);
Expand Down Expand Up @@ -426,7 +428,8 @@ abstract contract HorizonStakingSharedTest is GraphBaseTest {
serviceProvider,
thawingShares,
uint64(block.timestamp + beforeProvision.thawingPeriod),
expectedThawRequestId
expectedThawRequestId,
beforeProvision.thawingNonce
);
vm.expectEmit(address(staking));
emit IHorizonStakingMain.ProvisionThawed(serviceProvider, verifier, tokens);
Expand Down Expand Up @@ -461,6 +464,7 @@ abstract contract HorizonStakingSharedTest is GraphBaseTest {
assertEq(afterProvision.createdAt, beforeProvision.createdAt);
assertEq(afterProvision.maxVerifierCutPending, beforeProvision.maxVerifierCutPending);
assertEq(afterProvision.thawingPeriodPending, beforeProvision.thawingPeriodPending);
assertEq(afterProvision.lastParametersStagedAt, beforeProvision.lastParametersStagedAt);
assertEq(afterProvision.thawingNonce, beforeProvision.thawingNonce);
assertEq(thawRequestId, expectedThawRequestId);
assertEq(afterThawRequest.shares, thawingShares);
Expand Down Expand Up @@ -546,6 +550,7 @@ abstract contract HorizonStakingSharedTest is GraphBaseTest {
assertEq(afterProvision.createdAt, beforeProvision.createdAt);
assertEq(afterProvision.maxVerifierCutPending, beforeProvision.maxVerifierCutPending);
assertEq(afterProvision.thawingPeriodPending, beforeProvision.thawingPeriodPending);
assertEq(afterProvision.lastParametersStagedAt, beforeProvision.lastParametersStagedAt);
assertEq(afterProvision.thawingNonce, beforeProvision.thawingNonce);
assertEq(afterServiceProvider.tokensStaked, beforeServiceProvider.tokensStaked);
assertEq(
Expand Down Expand Up @@ -672,6 +677,7 @@ abstract contract HorizonStakingSharedTest is GraphBaseTest {
assertEq(afterProvision.createdAt, beforeValues.provision.createdAt);
assertEq(afterProvision.maxVerifierCutPending, beforeValues.provision.maxVerifierCutPending);
assertEq(afterProvision.thawingPeriodPending, beforeValues.provision.thawingPeriodPending);
assertEq(afterProvision.lastParametersStagedAt, beforeValues.provision.lastParametersStagedAt);
assertEq(afterProvision.thawingNonce, beforeValues.provision.thawingNonce);

// assert: provision new verifier
Expand Down Expand Up @@ -753,7 +759,9 @@ abstract contract HorizonStakingSharedTest is GraphBaseTest {
Provision memory beforeProvision = staking.getProvision(serviceProvider, verifier);

// setProvisionParameters
if (beforeProvision.maxVerifierCut != maxVerifierCut || beforeProvision.thawingPeriod != thawingPeriod) {
bool paramsChanged = beforeProvision.maxVerifierCut != maxVerifierCut ||
beforeProvision.thawingPeriod != thawingPeriod;
if (paramsChanged) {
vm.expectEmit();
emit IHorizonStakingMain.ProvisionParametersStaged(
serviceProvider,
Expand All @@ -776,6 +784,10 @@ abstract contract HorizonStakingSharedTest is GraphBaseTest {
assertEq(afterProvision.createdAt, beforeProvision.createdAt);
assertEq(afterProvision.maxVerifierCutPending, maxVerifierCut);
assertEq(afterProvision.thawingPeriodPending, thawingPeriod);
assertEq(
afterProvision.lastParametersStagedAt,
paramsChanged ? block.timestamp : beforeProvision.lastParametersStagedAt
);
assertEq(afterProvision.thawingNonce, beforeProvision.thawingNonce);
}

Expand Down Expand Up @@ -812,6 +824,7 @@ abstract contract HorizonStakingSharedTest is GraphBaseTest {
assertEq(afterProvision.thawingPeriod, beforeProvision.thawingPeriodPending);
assertEq(afterProvision.thawingPeriod, afterProvision.thawingPeriodPending);
assertEq(afterProvision.createdAt, beforeProvision.createdAt);
assertEq(afterProvision.lastParametersStagedAt, beforeProvision.lastParametersStagedAt);
assertEq(afterProvision.thawingNonce, beforeProvision.thawingNonce);
}

Expand Down Expand Up @@ -1007,10 +1020,11 @@ abstract contract HorizonStakingSharedTest is GraphBaseTest {
beneficiary,
calcValues.thawingShares,
calcValues.thawingUntil,
calcValues.thawRequestId
calcValues.thawRequestId,
beforeValues.pool.thawingNonce
);
vm.expectEmit();
emit IHorizonStakingMain.TokensUndelegated(serviceProvider, verifier, delegator, calcValues.tokens);
emit IHorizonStakingMain.TokensUndelegated(serviceProvider, verifier, delegator, calcValues.tokens, shares);
if (legacy) {
staking.undelegate(serviceProvider, shares);
} else if (thawRequestType == IHorizonStakingTypes.ThawRequestType.Delegation) {
Expand Down
72 changes: 72 additions & 0 deletions packages/horizon/test/staking/provision/thaw.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -162,4 +162,76 @@ contract HorizonStakingThawTest is HorizonStakingTest {
);
vm.assertEq(thawedTokens, thawAmount * thawSteps);
}

function testThaw_GetThawedTokens_AfterProvisionFullySlashed(
uint256 amount,
uint64 thawingPeriod,
uint256 thawAmount
) public useIndexer useProvision(amount, 0, thawingPeriod) {
// thaw some funds so there are some shares thawing and tokens thawing
thawAmount = bound(thawAmount, 1, amount);
_thaw(users.indexer, subgraphDataServiceAddress, thawAmount);

// skip to after the thawing period has passed
skip(thawingPeriod + 1);

// slash all of it
resetPrank(subgraphDataServiceAddress);
_slash(users.indexer, subgraphDataServiceAddress, amount, 0);

// get the thawed tokens - should be zero now as the pool was reset
uint256 thawedTokens = staking.getThawedTokens(
ThawRequestType.Provision,
users.indexer,
subgraphDataServiceAddress,
users.indexer
);
vm.assertEq(thawedTokens, 0);
}

function testThaw_GetThawedTokens_AfterRecoveringProvision(
uint256 amount,
uint64 thawingPeriod,
uint256 thawAmount
) public useIndexer useProvision(amount, 0, thawingPeriod) {
// thaw some funds so there are some shares thawing and tokens thawing
thawAmount = bound(thawAmount, 1, amount);
_thaw(users.indexer, subgraphDataServiceAddress, thawAmount);

// skip to after the thawing period has passed
skip(thawingPeriod + 1);

// slash all of it
resetPrank(subgraphDataServiceAddress);
_slash(users.indexer, subgraphDataServiceAddress, amount, 0);

// get the thawed tokens - should be zero now as the pool was reset
uint256 thawedTokens = staking.getThawedTokens(
ThawRequestType.Provision,
users.indexer,
subgraphDataServiceAddress,
users.indexer
);
vm.assertEq(thawedTokens, 0);

// put some funds back in
resetPrank(users.indexer);
_stake(amount);
_addToProvision(users.indexer, subgraphDataServiceAddress, amount);

// thaw some more funds
_thaw(users.indexer, subgraphDataServiceAddress, thawAmount);

// skip to after the thawing period has passed
skip(block.timestamp + thawingPeriod + 1);

// get the thawed tokens - should be the amount we thawed
thawedTokens = staking.getThawedTokens(
ThawRequestType.Provision,
users.indexer,
subgraphDataServiceAddress,
users.indexer
);
vm.assertEq(thawedTokens, thawAmount);
}
}
Loading
Loading