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

GraphPayments changes post audit #1078

Merged
merged 7 commits into from
Feb 10, 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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,10 @@ bin/
.vscode

# Coverage and other reports
coverage/
reports/
coverage.json
lcov.info

# Local test files
addresses-local.json
Expand Down
15 changes: 5 additions & 10 deletions packages/horizon/contracts/interfaces/IGraphPayments.sol
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ interface IGraphPayments {

/**
* @notice Emitted when a payment is collected
* @param paymentType The type of payment as defined in {IGraphPayments}
* @param payer The address of the payer
* @param receiver The address of the receiver
* @param dataService The address of the data service
Expand All @@ -30,8 +31,9 @@ interface IGraphPayments {
* @param tokensReceiver Amount of tokens for the receiver
*/
event GraphPaymentCollected(
PaymentTypes indexed paymentType,
address indexed payer,
address indexed receiver,
address receiver,
address indexed dataService,
uint256 tokens,
uint256 tokensProtocol,
Expand All @@ -40,14 +42,6 @@ interface IGraphPayments {
uint256 tokensReceiver
);

/**
* @notice Thrown when the calculated amount of tokens to be paid out to all parties is
* not the same as the amount of tokens being collected
* @param tokens The amount of tokens being collected
* @param tokensCalculated The sum of all the tokens to be paid out
*/
error GraphPaymentsBadAccounting(uint256 tokens, uint256 tokensCalculated);

/**
* @notice Thrown when the protocol payment cut is invalid
* @param protocolPaymentCut The protocol payment cut
Expand All @@ -63,9 +57,10 @@ interface IGraphPayments {
/**
* @notice Collects funds from a payer.
* It will pay cuts to all relevant parties and forward the rest to the receiver.
* 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 tokens The amount of tokens being collected.
* @param dataService The address of the data service
* @param dataServiceCut The data service cut in PPM
*/
Expand Down
33 changes: 29 additions & 4 deletions packages/horizon/contracts/interfaces/IPaymentsEscrow.sol
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,18 @@ interface IPaymentsEscrow {
/**
* @notice Emitted when a payer cancels an escrow thawing
* @param payer The address of the payer
* @param collector The address of the collector
* @param receiver The address of the receiver
* @param tokensThawing The amount of tokens that were being thawed
* @param thawEndTimestamp The timestamp at which the thawing period was ending
*/
event CancelThaw(address indexed payer, address indexed receiver);
event CancelThaw(
address indexed payer,
address indexed collector,
address indexed receiver,
uint256 tokensThawing,
uint256 thawEndTimestamp
);

/**
* @notice Emitted when a payer thaws funds from the escrow for a payer-collector-receiver tuple
Expand Down Expand Up @@ -68,12 +77,19 @@ interface IPaymentsEscrow {

/**
* @notice Emitted when a collector collects funds from the escrow for a payer-collector-receiver tuple
* @param paymentType The type of payment being collected as defined in the {IGraphPayments} interface
* @param payer The address of the payer
* @param collector The address of the collector
* @param receiver The address of the receiver
* @param tokens The amount of tokens collected
*/
event EscrowCollected(address indexed payer, address indexed collector, address indexed receiver, uint256 tokens);
event EscrowCollected(
IGraphPayments.PaymentTypes indexed paymentType,
address indexed payer,
address indexed collector,
address receiver,
uint256 tokens
);

// -- Errors --

Expand Down Expand Up @@ -145,20 +161,29 @@ interface IPaymentsEscrow {
/**
* @notice Thaw a specific amount of escrow from a payer-collector-receiver's escrow account.
* The payer is the transaction caller.
* If `tokens` is zero and funds were already thawing it will cancel the thawing.
* Note that repeated calls to this function will overwrite the previous thawing amount
* and reset the thawing period.
* @dev Requirements:
* - `tokens` must be less than or equal to the available balance
*
* Emits a {Thaw} event. If `tokens` is zero it will emit a {CancelThaw} event.
* Emits a {Thaw} event.
*
* @param collector The address of the collector
* @param receiver The address of the receiver
* @param tokens The amount of tokens to thaw
*/
function thaw(address collector, address receiver, uint256 tokens) external;

/**
* @notice Cancels the thawing of escrow from a payer-collector-receiver's escrow account.
* @param collector The address of the collector
* @param receiver The address of the receiver
* @dev Requirements:
* - The payer must be thawing funds
* Emits a {CancelThaw} event.
*/
function cancelThaw(address collector, address receiver) external;

/**
* @notice Withdraws all thawed escrow from a payer-collector-receiver's escrow account.
* The payer is the transaction caller.
Expand Down
4 changes: 2 additions & 2 deletions packages/horizon/contracts/interfaces/ITAPCollector.sol
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@ interface ITAPCollector is IPaymentsCollector {
bytes32 collectionId;
// The address of the payer the RAV was issued by
address payer;
// The address of the data service the RAV was issued to
address dataService;
// The address of the service provider the RAV was issued to
address serviceProvider;
// The address of the data service the RAV was issued to
address dataService;
// The RAV timestamp, indicating the latest TAP Receipt in the RAV
uint64 timestampNs;
// Total amount owed to the service provider since the beginning of the
Expand Down
20 changes: 12 additions & 8 deletions packages/horizon/contracts/payments/GraphPayments.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ pragma solidity 0.8.27;

import { IGraphToken } from "@graphprotocol/contracts/contracts/token/IGraphToken.sol";
import { IGraphPayments } from "../interfaces/IGraphPayments.sol";
import { IHorizonStakingTypes } from "../interfaces/internal/IHorizonStakingTypes.sol";

import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import { MulticallUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/MulticallUpgradeable.sol";
Expand All @@ -22,6 +23,8 @@ import { GraphDirectory } from "../utilities/GraphDirectory.sol";
contract GraphPayments is Initializable, MulticallUpgradeable, GraphDirectory, IGraphPayments {
using TokenUtils for IGraphToken;
using PPMMath for uint256;

/// @notice Protocol payment cut in PPM
uint256 public immutable PROTOCOL_PAYMENT_CUT;

/**
Expand Down Expand Up @@ -70,14 +73,14 @@ contract GraphPayments is Initializable, MulticallUpgradeable, GraphDirectory, I
uint256 tokensDataService = tokensRemaining.mulPPMRoundUp(dataServiceCut);
tokensRemaining = tokensRemaining - tokensDataService;

uint256 tokensDelegationPool = tokensRemaining.mulPPMRoundUp(
_graphStaking().getDelegationFeeCut(receiver, dataService, paymentType)
);
tokensRemaining = tokensRemaining - tokensDelegationPool;

// Ensure accounting is correct
uint256 tokensTotal = tokensProtocol + tokensDataService + tokensDelegationPool + tokensRemaining;
require(tokens == tokensTotal, GraphPaymentsBadAccounting(tokens, tokensTotal));
uint256 tokensDelegationPool = 0;
IHorizonStakingTypes.DelegationPool memory pool = _graphStaking().getDelegationPool(receiver, dataService);
if (pool.shares > 0) {
tokensDelegationPool = tokensRemaining.mulPPMRoundUp(
_graphStaking().getDelegationFeeCut(receiver, dataService, paymentType)
);
tokensRemaining = tokensRemaining - tokensDelegationPool;
}

// Pay all parties
_graphToken().burnTokens(tokensProtocol);
Expand All @@ -92,6 +95,7 @@ contract GraphPayments is Initializable, MulticallUpgradeable, GraphDirectory, I
_graphToken().pushTokens(receiver, tokensRemaining);

emit GraphPaymentCollected(
paymentType,
msg.sender,
receiver,
dataService,
Expand Down
57 changes: 32 additions & 25 deletions packages/horizon/contracts/payments/PaymentsEscrow.sol
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,21 @@ import { GraphDirectory } from "../utilities/GraphDirectory.sol";
contract PaymentsEscrow is Initializable, MulticallUpgradeable, GraphDirectory, IPaymentsEscrow {
using TokenUtils for IGraphToken;

/// @notice Escrow account details for payer-collector-receiver tuples
mapping(address payer => mapping(address collector => mapping(address receiver => IPaymentsEscrow.EscrowAccount escrowAccount)))
public escrowAccounts;

/// @notice The maximum thawing period (in seconds) for both escrow withdrawal and collector revocation
/// @dev This is a precautionary measure to avoid inadvertedly locking funds for too long
uint256 public constant MAX_WAIT_PERIOD = 90 days;

/// @notice Thawing period in seconds for escrow funds withdrawal
uint256 public immutable WITHDRAW_ESCROW_THAWING_PERIOD;

/// @notice Escrow account details for payer-collector-receiver tuples
mapping(address payer => mapping(address collector => mapping(address receiver => IPaymentsEscrow.EscrowAccount escrowAccount)))
public escrowAccounts;

/**
* @notice Modifier to prevent function execution when contract is paused
* @dev Reverts if the controller indicates the contract is paused
*/
modifier notPaused() {
require(!_graphController().paused(), PaymentsEscrowIsPaused());
_;
Expand Down Expand Up @@ -78,19 +82,9 @@ contract PaymentsEscrow is Initializable, MulticallUpgradeable, GraphDirectory,
* @notice See {IPaymentsEscrow-thaw}
*/
function thaw(address collector, address receiver, uint256 tokens) external override notPaused {
EscrowAccount storage account = escrowAccounts[msg.sender][collector][receiver];

// if amount thawing is zero and requested amount is zero this is an invalid request.
// otherwise if amount thawing is greater than zero and requested amount is zero this
// is a cancel thaw request.
if (tokens == 0) {
require(account.tokensThawing != 0, PaymentsEscrowNotThawing());
account.tokensThawing = 0;
account.thawEndTimestamp = 0;
emit CancelThaw(msg.sender, receiver);
return;
}
require(tokens > 0, PaymentsEscrowInvalidZeroTokens());

EscrowAccount storage account = escrowAccounts[msg.sender][collector][receiver];
require(account.balance >= tokens, PaymentsEscrowInsufficientBalance(account.balance, tokens));

account.tokensThawing = tokens;
Expand All @@ -99,6 +93,21 @@ contract PaymentsEscrow is Initializable, MulticallUpgradeable, GraphDirectory,
emit Thaw(msg.sender, collector, receiver, tokens, account.thawEndTimestamp);
}

/**
* @notice See {IPaymentsEscrow-cancelThaw}
*/
function cancelThaw(address collector, address receiver) external override notPaused {
EscrowAccount storage account = escrowAccounts[msg.sender][collector][receiver];
require(account.tokensThawing != 0, PaymentsEscrowNotThawing());

uint256 tokensThawing = account.tokensThawing;
uint256 thawEndTimestamp = account.thawEndTimestamp;
account.tokensThawing = 0;
account.thawEndTimestamp = 0;

emit CancelThaw(msg.sender, collector, receiver, tokensThawing, thawEndTimestamp);
}

/**
* @notice See {IPaymentsEscrow-withdraw}
*/
Expand Down Expand Up @@ -138,29 +147,27 @@ contract PaymentsEscrow is Initializable, MulticallUpgradeable, GraphDirectory,
// Reduce amount from account balance
account.balance -= tokens;

uint256 balanceBefore = _graphToken().balanceOf(address(this));
uint256 escrowBalanceBefore = _graphToken().balanceOf(address(this));

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

uint256 balanceAfter = _graphToken().balanceOf(address(this));
// Verify that the escrow balance is consistent with the collected tokens
uint256 escrowBalanceAfter = _graphToken().balanceOf(address(this));
require(
balanceBefore == tokens + balanceAfter,
PaymentsEscrowInconsistentCollection(balanceBefore, balanceAfter, tokens)
escrowBalanceBefore == tokens + escrowBalanceAfter,
PaymentsEscrowInconsistentCollection(escrowBalanceBefore, escrowBalanceAfter, tokens)
);

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

/**
* @notice See {IPaymentsEscrow-getBalance}
*/
function getBalance(address payer, address collector, address receiver) external view override returns (uint256) {
EscrowAccount storage account = escrowAccounts[payer][collector][receiver];
if (account.balance <= account.tokensThawing) {
return 0;
}
return account.balance - account.tokensThawing;
return account.balance > account.tokensThawing ? account.balance - account.tokensThawing : 0;
}

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

/// @notice Authorization details for payer-signer pairs
Expand Down Expand Up @@ -166,8 +166,9 @@ contract TAPCollector is EIP712, GraphDirectory, ITAPCollector {
bytes memory _data,
uint256 _tokensToCollect
) private returns (uint256) {
// Ensure caller is the RAV data service
(SignedRAV memory signedRAV, uint256 dataServiceCut) = abi.decode(_data, (SignedRAV, uint256));

// Ensure caller is the RAV data service
require(
signedRAV.rav.dataService == msg.sender,
TAPCollectorCallerNotDataService(msg.sender, signedRAV.rav.dataService)
Expand Down Expand Up @@ -259,8 +260,8 @@ contract TAPCollector is EIP712, GraphDirectory, ITAPCollector {
abi.encode(
EIP712_RAV_TYPEHASH,
_rav.payer,
_rav.dataService,
_rav.serviceProvider,
_rav.dataService,
_rav.timestampNs,
_rav.valueAggregate,
keccak256(_rav.metadata)
Expand Down
Loading