Skip to content

Horizon: pending changes after audit #1131

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

Merged
merged 3 commits into from
May 20, 2025
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
14 changes: 11 additions & 3 deletions packages/horizon/contracts/interfaces/IGraphPayments.sol
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ interface IGraphPayments {
* @param tokensDataService Amount of tokens for the data service
* @param tokensDelegationPool Amount of tokens for delegators
* @param tokensReceiver Amount of tokens for the receiver
* @param receiverDestination The address where the receiver's payment cut is sent.
*/
event GraphPaymentCollected(
PaymentTypes indexed paymentType,
Expand All @@ -41,7 +42,8 @@ interface IGraphPayments {
uint256 tokensProtocol,
uint256 tokensDataService,
uint256 tokensDelegationPool,
uint256 tokensReceiver
uint256 tokensReceiver,
address receiverDestination
);

/**
Expand All @@ -63,19 +65,25 @@ interface IGraphPayments {

/**
* @notice Collects funds from a payer.
* It will pay cuts to all relevant parties and forward the rest to the receiver.
* It will pay cuts to all relevant parties and forward the rest to the receiver destination address. If the
* destination address is zero the funds are automatically staked to the receiver. Note that the receiver
* destination address can be set to the receiver address to collect funds on the receiver without re-staking.
*
* Note that the collected amount can be zero.
*
* @param paymentType The type of payment as defined in {IGraphPayments}
* @param receiver The address of the receiver
* @param tokens The amount of tokens being collected.
* @param dataService The address of the data service
* @param dataServiceCut The data service cut in PPM
* @param receiverDestination The address where the receiver's payment cut is sent.
*/
function collect(
PaymentTypes paymentType,
address receiver,
uint256 tokens,
address dataService,
uint256 dataServiceCut
uint256 dataServiceCut,
address receiverDestination
) external;
}
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,10 @@ interface IGraphTallyCollector is IPaymentsCollector {
* - The amount of tokens to collect must be less than or equal to the total amount of tokens in the RAV minus
* the tokens already collected.
* @param paymentType The payment type to collect
* @param data Additional data required for the payment collection
* @param data Additional data required for the payment collection. Encoded as follows:
* - SignedRAV `signedRAV`: The signed RAV
* - uint256 `dataServiceCut`: The data service cut in PPM
* - address `receiverDestination`: The address where the receiver's payment should be sent.
* @param tokensToCollect The amount of tokens to collect
* @return The amount of tokens collected
*/
Expand Down
8 changes: 6 additions & 2 deletions packages/horizon/contracts/interfaces/IPaymentsEscrow.sol
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,15 @@ interface IPaymentsEscrow {
* @param collector The address of the collector
* @param receiver The address of the receiver
* @param tokens The amount of tokens collected
* @param receiverDestination The address where the receiver's payment should be sent.
*/
event EscrowCollected(
IGraphPayments.PaymentTypes indexed paymentType,
address indexed payer,
address indexed collector,
address receiver,
uint256 tokens
uint256 tokens,
address receiverDestination
);

// -- Errors --
Expand Down Expand Up @@ -221,14 +223,16 @@ interface IPaymentsEscrow {
* @param tokens The amount of tokens to collect
* @param dataService The address of the data service
* @param dataServiceCut The data service cut in PPM that {GraphPayments} should send
* @param receiverDestination The address where the receiver's payment should be sent.
*/
function collect(
IGraphPayments.PaymentTypes paymentType,
address payer,
address receiver,
uint256 tokens,
address dataService,
uint256 dataServiceCut
uint256 dataServiceCut,
address receiverDestination
) external;

/**
Expand Down
15 changes: 12 additions & 3 deletions packages/horizon/contracts/payments/GraphPayments.sol
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ contract GraphPayments is Initializable, MulticallUpgradeable, GraphDirectory, I
address receiver,
uint256 tokens,
address dataService,
uint256 dataServiceCut
uint256 dataServiceCut,
address receiverDestination
) external {
require(PPMMath.isValidPPM(dataServiceCut), GraphPaymentsInvalidCut(dataServiceCut));

Expand Down Expand Up @@ -88,7 +89,14 @@ contract GraphPayments is Initializable, MulticallUpgradeable, GraphDirectory, I
_graphStaking().addToDelegationPool(receiver, dataService, tokensDelegationPool);
}

_graphToken().pushTokens(receiver, tokensRemaining);
if (tokensRemaining > 0) {
if (receiverDestination == address(0)) {
_graphToken().approve(address(_graphStaking()), tokensRemaining);
_graphStaking().stakeTo(receiver, tokensRemaining);
} else {
_graphToken().pushTokens(receiverDestination, tokensRemaining);
}
}

emit GraphPaymentCollected(
paymentType,
Expand All @@ -99,7 +107,8 @@ contract GraphPayments is Initializable, MulticallUpgradeable, GraphDirectory, I
tokensProtocol,
tokensDataService,
tokensDelegationPool,
tokensRemaining
tokensRemaining,
receiverDestination
);
}
}
7 changes: 4 additions & 3 deletions packages/horizon/contracts/payments/PaymentsEscrow.sol
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,8 @@ contract PaymentsEscrow is Initializable, MulticallUpgradeable, GraphDirectory,
address receiver,
uint256 tokens,
address dataService,
uint256 dataServiceCut
uint256 dataServiceCut,
address receiverDestination
) external override notPaused {
// Check if there are enough funds in the escrow account
EscrowAccount storage account = escrowAccounts[payer][msg.sender][receiver];
Expand All @@ -137,7 +138,7 @@ contract PaymentsEscrow is Initializable, MulticallUpgradeable, GraphDirectory,
uint256 escrowBalanceBefore = _graphToken().balanceOf(address(this));

_graphToken().approve(address(_graphPayments()), tokens);
_graphPayments().collect(paymentType, receiver, tokens, dataService, dataServiceCut);
_graphPayments().collect(paymentType, receiver, tokens, dataService, dataServiceCut, receiverDestination);

// Verify that the escrow balance is consistent with the collected tokens
uint256 escrowBalanceAfter = _graphToken().balanceOf(address(this));
Expand All @@ -146,7 +147,7 @@ contract PaymentsEscrow is Initializable, MulticallUpgradeable, GraphDirectory,
PaymentsEscrowInconsistentCollection(escrowBalanceBefore, escrowBalanceAfter, tokens)
);

emit EscrowCollected(paymentType, payer, msg.sender, receiver, tokens);
emit EscrowCollected(paymentType, payer, msg.sender, receiver, tokens, receiverDestination);
}

/// @inheritdoc IPaymentsEscrow
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@ contract GraphTallyCollector is EIP712, GraphDirectory, Authorizable, IGraphTall
* - Signer of the RAV must be authorized to sign for the payer.
* - Service provider must have an active provision with the data service to collect payments.
* @notice REVERT: This function may revert if ECDSA.recover fails, check ECDSA library for details.
* @param paymentType The payment type to collect
* @param data Additional data required for the payment collection. Encoded as follows:
* - SignedRAV `signedRAV`: The signed RAV
* - uint256 `dataServiceCut`: The data service cut in PPM
* - address `receiverDestination`: The address where the receiver's payment should be sent.
* @return The amount of tokens collected
*/
/// @inheritdoc IPaymentsCollector
function collect(IGraphPayments.PaymentTypes paymentType, bytes calldata data) external override returns (uint256) {
Expand Down Expand Up @@ -96,7 +102,7 @@ contract GraphTallyCollector is EIP712, GraphDirectory, Authorizable, IGraphTall
bytes calldata _data,
uint256 _tokensToCollect
) private returns (uint256) {
(SignedRAV memory signedRAV, uint256 dataServiceCut) = abi.decode(_data, (SignedRAV, uint256));
(SignedRAV memory signedRAV, uint256 dataServiceCut, address receiverDestination) = abi.decode(_data, (SignedRAV, uint256, address));

// Ensure caller is the RAV data service
require(
Expand All @@ -123,10 +129,9 @@ contract GraphTallyCollector is EIP712, GraphDirectory, Authorizable, IGraphTall
}

uint256 tokensToCollect = 0;
address payer = signedRAV.rav.payer;
{
uint256 tokensRAV = signedRAV.rav.valueAggregate;
uint256 tokensAlreadyCollected = tokensCollected[dataService][collectionId][receiver][payer];
uint256 tokensAlreadyCollected = tokensCollected[dataService][collectionId][receiver][signedRAV.rav.payer];
require(
tokensRAV > tokensAlreadyCollected,
GraphTallyCollectorInconsistentRAVTokens(tokensRAV, tokensAlreadyCollected)
Expand All @@ -147,16 +152,16 @@ contract GraphTallyCollector is EIP712, GraphDirectory, Authorizable, IGraphTall
}

if (tokensToCollect > 0) {
tokensCollected[dataService][collectionId][receiver][payer] += tokensToCollect;
_graphPaymentsEscrow().collect(_paymentType, payer, receiver, tokensToCollect, dataService, dataServiceCut);
tokensCollected[dataService][collectionId][receiver][signedRAV.rav.payer] += tokensToCollect;
_graphPaymentsEscrow().collect(_paymentType, signedRAV.rav.payer, receiver, tokensToCollect, dataService, dataServiceCut, receiverDestination);
}

emit PaymentCollected(_paymentType, collectionId, payer, receiver, dataService, tokensToCollect);
emit PaymentCollected(_paymentType, collectionId, signedRAV.rav.payer, receiver, dataService, tokensToCollect);

// This event is emitted to allow reconstructing RAV history with onchain data.
emit RAVCollected(
collectionId,
payer,
signedRAV.rav.payer,
receiver,
dataService,
signedRAV.rav.timestampNs,
Expand Down
54 changes: 40 additions & 14 deletions packages/horizon/test/escrow/GraphEscrow.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -113,13 +113,21 @@ contract GraphEscrowTest is HorizonStakingSharedTest, PaymentsEscrowSharedTest {
uint256 payerEscrowBalance;
}

struct CollectTokensData {
uint256 tokensProtocol;
uint256 tokensDataService;
uint256 tokensDelegation;
uint256 receiverExpectedPayment;
}

function _collectEscrow(
IGraphPayments.PaymentTypes _paymentType,
address _payer,
address _receiver,
uint256 _tokens,
address _dataService,
uint256 _dataServiceCut
uint256 _dataServiceCut,
address _paymentsDestination
) internal {
(, address _collector, ) = vm.readCallers();

Expand All @@ -132,31 +140,49 @@ contract GraphEscrowTest is HorizonStakingSharedTest, PaymentsEscrowSharedTest {
dataServiceBalance: token.balanceOf(_dataService),
payerEscrowBalance: 0
});
CollectTokensData memory collectTokensData = CollectTokensData({
tokensProtocol: 0,
tokensDataService: 0,
tokensDelegation: 0,
receiverExpectedPayment: 0
});

{
(uint256 payerEscrowBalance, , ) = escrow.escrowAccounts(_payer, _collector, _receiver);
previousBalances.payerEscrowBalance = payerEscrowBalance;
}

vm.expectEmit(address(escrow));
emit IPaymentsEscrow.EscrowCollected(_paymentType, _payer, _collector, _receiver, _tokens);
escrow.collect(_paymentType, _payer, _receiver, _tokens, _dataService, _dataServiceCut);
emit IPaymentsEscrow.EscrowCollected(
_paymentType,
_payer,
_collector,
_receiver,
_tokens,
_paymentsDestination
);
escrow.collect(_paymentType, _payer, _receiver, _tokens, _dataService, _dataServiceCut, _paymentsDestination);

// Calculate cuts
// this is nasty but stack is indeed too deep
uint256 tokensDataService = (_tokens - _tokens.mulPPMRoundUp(payments.PROTOCOL_PAYMENT_CUT())).mulPPMRoundUp(
collectTokensData.tokensProtocol = _tokens.mulPPMRoundUp(payments.PROTOCOL_PAYMENT_CUT());
collectTokensData.tokensDataService = (_tokens - collectTokensData.tokensProtocol).mulPPMRoundUp(
_dataServiceCut
);
uint256 tokensDelegation = 0;

IHorizonStakingTypes.DelegationPool memory pool = staking.getDelegationPool(_receiver, _dataService);
if (pool.shares > 0) {
tokensDelegation = (_tokens - _tokens.mulPPMRoundUp(payments.PROTOCOL_PAYMENT_CUT()) - tokensDataService)
.mulPPMRoundUp(staking.getDelegationFeeCut(_receiver, _dataService, _paymentType));
collectTokensData.tokensDelegation = (_tokens -
collectTokensData.tokensProtocol -
collectTokensData.tokensDataService).mulPPMRoundUp(
staking.getDelegationFeeCut(_receiver, _dataService, _paymentType)
);
}
uint256 receiverExpectedPayment = _tokens -
_tokens.mulPPMRoundUp(payments.PROTOCOL_PAYMENT_CUT()) -
tokensDataService -
tokensDelegation;
collectTokensData.receiverExpectedPayment =
_tokens -
collectTokensData.tokensProtocol -
collectTokensData.tokensDataService -
collectTokensData.tokensDelegation;

// After balances
CollectPaymentData memory afterBalances = CollectPaymentData({
Expand All @@ -173,11 +199,11 @@ contract GraphEscrowTest is HorizonStakingSharedTest, PaymentsEscrowSharedTest {
}

// Check receiver balance after payment
assertEq(afterBalances.receiverBalance - previousBalances.receiverBalance, receiverExpectedPayment);
assertEq(afterBalances.receiverBalance - previousBalances.receiverBalance, collectTokensData.receiverExpectedPayment);
assertEq(token.balanceOf(address(payments)), 0);

// Check delegation pool balance after payment
assertEq(afterBalances.delegationPoolBalance - previousBalances.delegationPoolBalance, tokensDelegation);
assertEq(afterBalances.delegationPoolBalance - previousBalances.delegationPoolBalance, collectTokensData.tokensDelegation);

// Check that the escrow account has been updated
assertEq(previousBalances.escrowBalance, afterBalances.escrowBalance + _tokens);
Expand All @@ -186,7 +212,7 @@ contract GraphEscrowTest is HorizonStakingSharedTest, PaymentsEscrowSharedTest {
assertEq(previousBalances.paymentsBalance, afterBalances.paymentsBalance);

// Check data service balance after payment
assertEq(afterBalances.dataServiceBalance - previousBalances.dataServiceBalance, tokensDataService);
assertEq(afterBalances.dataServiceBalance - previousBalances.dataServiceBalance, collectTokensData.tokensDataService);

// Check payers escrow balance after payment
assertEq(previousBalances.payerEscrowBalance - _tokens, afterBalances.payerEscrowBalance);
Expand Down
21 changes: 8 additions & 13 deletions packages/horizon/test/escrow/collect.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ contract GraphEscrowCollectTest is GraphEscrowTest {
users.indexer,
tokensToCollect,
subgraphDataServiceAddress,
dataServiceCut
dataServiceCut,
users.indexer
);
}

Expand All @@ -71,7 +72,8 @@ contract GraphEscrowCollectTest is GraphEscrowTest {
users.indexer,
tokens,
subgraphDataServiceAddress,
dataServiceCut
dataServiceCut,
users.indexer
);
}

Expand All @@ -95,7 +97,8 @@ contract GraphEscrowCollectTest is GraphEscrowTest {
users.indexer,
amount,
subgraphDataServiceAddress,
0
0,
users.indexer
);
vm.stopPrank();
}
Expand Down Expand Up @@ -126,16 +129,8 @@ contract GraphEscrowCollectTest is GraphEscrowTest {
users.indexer,
firstCollect,
subgraphDataServiceAddress,
0
0,
users.indexer
);

// _collectEscrow(
// IGraphPayments.PaymentTypes.QueryFee,
// users.gateway,
// users.indexer,
// secondCollect,
// subgraphDataServiceAddress,
// 0
// );
}
}
3 changes: 2 additions & 1 deletion packages/horizon/test/escrow/getters.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ contract GraphEscrowGettersTest is GraphEscrowTest {
users.indexer,
amountCollected,
subgraphDataServiceAddress,
0
0,
users.indexer
);

// balance should always be 0 since thawing funds > available funds
Expand Down
3 changes: 2 additions & 1 deletion packages/horizon/test/escrow/paused.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ contract GraphEscrowPausedTest is GraphEscrowTest {
users.indexer,
tokens,
subgraphDataServiceAddress,
tokensDataService
tokensDataService,
users.indexer
);
}
}
3 changes: 2 additions & 1 deletion packages/horizon/test/escrow/withdraw.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ contract GraphEscrowWithdrawTest is GraphEscrowTest {
users.indexer,
amountCollected,
subgraphDataServiceAddress,
0
0,
users.indexer
);

// Advance time to simulate the thawing period
Expand Down
Loading
Loading