Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
3 changes: 3 additions & 0 deletions packages/horizon/contracts/interfaces/IPaymentsCollector.sol
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ interface IPaymentsCollector {
* @notice Initiate a payment collection through the payments protocol
* @dev This function should require the caller to present some form of evidence of the payer's debt to
* the receiver. The collector should validate this evidence and, if valid, collect the payment.
* Requirements:
* - The caller must be the data service the RAV was issued to
* - The signer of the RAV must be authorized to sign for the payer
*
* Emits a {PaymentCollected} event
*
Expand Down
131 changes: 131 additions & 0 deletions packages/horizon/contracts/interfaces/ITAPCollector.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,15 @@ import { IPaymentsCollector } from "./IPaymentsCollector.sol";
* payments using a TAP RAV (Receipt Aggregate Voucher).
*/
interface ITAPCollector is IPaymentsCollector {
/// @notice Details for a payer-signer pair
/// @dev Signers can be removed only after a thawing period
struct PayerAuthorization {
// Payer the signer is authorized to sign for
address payer;
// Timestamp at which thawing period ends (zero if not thawing)
uint256 thawEndTimestamp;
}

/// @notice The Receipt Aggregate Voucher (RAV) struct
struct ReceiptAggregateVoucher {
// The address of the data service the RAV was issued to
Expand All @@ -34,6 +43,36 @@ interface ITAPCollector is IPaymentsCollector {
bytes signature;
}

/**
* @notice Emitted when a signer is authorized to sign RAVs for a payer
* @param authorizedSigner The address of the authorized signer
* @param payer The address of the payer authorizing the signer
*/
event AuthorizeSigner(address indexed authorizedSigner, address indexed payer);

/**
* @notice Emitted when a signer is thawed to be removed from the authorized signers list
* @param payer The address of the payer thawing the signer
* @param authorizedSigner The address of the signer to thaw
* @param thawEndTimestamp The timestamp at which the thawing period ends
*/
event ThawSigner(address indexed payer, address indexed authorizedSigner, uint256 thawEndTimestamp);

/**
* @dev Emitted when the thawing of a signer is cancelled
* @param payer The address of the payer cancelling the thawing
* @param authorizedSigner The address of the authorized signer
* @param thawEndTimestamp The timestamp at which the thawing period ends
*/
event CancelThawSigner(address indexed payer, address indexed authorizedSigner, uint256 thawEndTimestamp);

/**
* @dev Emitted when a authorized signer has been revoked
* @param payer The address of the payer revoking the signer
* @param authorizedSigner The address of the authorized signer
*/
event RevokeAuthorizedSigner(address indexed payer, address indexed authorizedSigner);

/**
* @notice Emitted when a RAV is collected
* @param payer The address of the payer
Expand All @@ -54,6 +93,50 @@ interface ITAPCollector is IPaymentsCollector {
bytes signature
);

/**
* Thrown when the signer is already authorized
* @param signer The address of the signer
* @param authorizingPayer The address of the payer authorizing the signer
*/
error TAPCollectorSignerAlreadyAuthorized(address signer, address authorizingPayer);

/**
* Thrown when the signer proof deadline is invalid
* @param proofDeadline The deadline for the proof provided by the signer
* @param currentTimestamp The current timestamp
*/
error TAPCollectorInvalidSignerProofDeadline(uint256 proofDeadline, uint256 currentTimestamp);

/**
* Thrown when the signer proof is invalid
*/
error TAPCollectorInvalidSignerProof();

/**
* Thrown when the signer is not authorized by the payer
* @param signer The address of the signer
* @param payer The address of the payer
*/
error TAPCollectorSignerNotAuthorizedByPayer(address signer, address payer);

/**
* Thrown when the signer is not thawing
* @param signer The address of the signer
*/
error TAPCollectorSignerNotThawing(address signer);

/**
* Thrown when the signer is still thawing
* @param currentTimestamp The current timestamp
* @param thawEndTimestamp The timestamp at which the thawing period ends
*/
error TAPCollectorSignerStillThawing(uint256 currentTimestamp, uint256 thawEndTimestamp);

/**
* Thrown when the RAV signer is invalid
*/
error TAPCollectorInvalidRAVSigner();

/**
* Thrown when the caller is not the data service the RAV was issued to
* @param caller The address of the caller
Expand All @@ -69,6 +152,54 @@ interface ITAPCollector is IPaymentsCollector {
*/
error TAPCollectorInconsistentRAVTokens(uint256 tokens, uint256 tokensCollected);

/**
* @notice Authorize a signer to sign on behalf of the payer
* @dev Requirements:
* - `signer` must not be already authorized
* - `proofDeadline` must be greater than the current timestamp
* - `proof` must be a valid signature from the signer being authorized
*
* Emits an {AuthorizedSigner} event
* @param signer The addres of the authorized signer
* @param proofDeadline The deadline for the proof provided by the signer
* @param proof The proof provided by the signer to be authorized by the payer, consists of (chainID, proof deadline, sender address)
*/
function authorizeSigner(address signer, uint256 proofDeadline, bytes calldata proof) external;

/**
* @notice Starts thawing a signer to be removed from the authorized signers list
* @dev Thawing a signer alerts receivers that signatures from that signer will soon be deemed invalid.
* Receivers without existing signed receipts or RAVs from this signer should treat them as unauthorized.
* Those with existing signed documents from this signer should work towards settling their engagements.
* Once a signer is thawed, they should be viewed as revoked regardless of their revocation status.
* Requirements:
* - `signer` must be authorized by the payer calling this function
*
* Emits a {ThawSigner} event
* @param signer The address of the signer to thaw
*/
function thawSigner(address signer) external;

/**
* @notice Stops thawing a signer.
* @dev Requirements:
* - `signer` must be thawing and authorized by the payer calling this function
*
* Emits a {CancelThawSigner} event
* @param signer The address of the signer to cancel thawing
*/
function cancelThawSigner(address signer) external;

/**
* @notice Revokes a signer from the authorized signers list if thawed.
* @dev Requirements:
* - `signer` must be thawed and authorized by the payer calling this function
*
* Emits a {RevokeAuthorizedSigner} event
* @param signer The address of the signer
*/
function revokeAuthorizedSigner(address signer) external;

/**
* @dev Recovers the signer address of a signed ReceiptAggregateVoucher (RAV).
* @param signedRAV The SignedRAV containing the RAV and its signature.
Expand Down
182 changes: 144 additions & 38 deletions packages/horizon/contracts/payments/collectors/TAPCollector.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { PPMMath } from "../../libraries/PPMMath.sol";

import { GraphDirectory } from "../../utilities/GraphDirectory.sol";
import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import { MessageHashUtils } from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";

/**
* @title TAPCollector contract
Expand All @@ -29,21 +30,89 @@ contract TAPCollector is EIP712, GraphDirectory, ITAPCollector {
"ReceiptAggregateVoucher(address dataService,address serviceProvider,uint64 timestampNs,uint128 valueAggregate,bytes metadata)"
);

/// @notice Authorization details for payer-signer pairs
mapping(address signer => PayerAuthorization authorizedSigner) public authorizedSigners;

/// @notice Tracks the amount of tokens already collected by a data service from a payer to a receiver
mapping(address dataService => mapping(address receiver => mapping(address payer => uint256 tokens)))
public tokensCollected;

/// @notice The duration (in seconds) in which a signer is thawing before they can be revoked
uint256 public immutable revokeSignerThawingPeriod;

/**
* @notice Constructs a new instance of the TAPVerifier contract.
* @param eip712Name The name of the EIP712 domain.
* @param eip712Version The version of the EIP712 domain.
* @param controller The address of the Graph controller.
* @param _revokeSignerThawingPeriod The duration (in seconds) in which a signer is thawing before they can be revoked.
*/
constructor(
string memory eip712Name,
string memory eip712Version,
address controller
) EIP712(eip712Name, eip712Version) GraphDirectory(controller) {}
address controller,
uint256 _revokeSignerThawingPeriod
) EIP712(eip712Name, eip712Version) GraphDirectory(controller) {
revokeSignerThawingPeriod = _revokeSignerThawingPeriod;
}

/**
* See {ITAPCollector.authorizeSigner}.
*/
function authorizeSigner(address signer, uint256 proofDeadline, bytes calldata proof) external override {
require(
authorizedSigners[signer].payer == address(0),
TAPCollectorSignerAlreadyAuthorized(signer, authorizedSigners[signer].payer)
);

verifyAuthorizedSignerProof(proof, proofDeadline, signer);

authorizedSigners[signer].payer = msg.sender;
authorizedSigners[signer].thawEndTimestamp = 0;
emit AuthorizeSigner(signer, msg.sender);
}

/**
* See {ITAPCollector.thawSigner}.
*/
function thawSigner(address signer) external override {
PayerAuthorization storage authorization = authorizedSigners[signer];

require(authorization.payer == msg.sender, TAPCollectorSignerNotAuthorizedByPayer(signer, msg.sender));

authorization.thawEndTimestamp = block.timestamp + revokeSignerThawingPeriod;
emit ThawSigner(msg.sender, signer, authorization.thawEndTimestamp);
}

/**
* See {ITAPCollector.cancelThawSigner}.
*/
function cancelThawSigner(address signer) external override {
PayerAuthorization storage authorization = authorizedSigners[signer];

require(authorization.payer == msg.sender, TAPCollectorSignerNotAuthorizedByPayer(signer, msg.sender));
require(authorization.thawEndTimestamp > 0, TAPCollectorSignerNotThawing(signer));

authorization.thawEndTimestamp = 0;
emit CancelThawSigner(msg.sender, signer, 0);
}

/**
* See {ITAPCollector.revokeAuthorizedSigner}.
*/
function revokeAuthorizedSigner(address signer) external override {
PayerAuthorization storage authorization = authorizedSigners[signer];

require(authorization.payer == msg.sender, TAPCollectorSignerNotAuthorizedByPayer(signer, msg.sender));
require(authorization.thawEndTimestamp > 0, TAPCollectorSignerNotThawing(signer));
require(
authorization.thawEndTimestamp <= block.timestamp,
TAPCollectorSignerStillThawing(block.timestamp, authorization.thawEndTimestamp)
);

delete authorizedSigners[signer];
emit RevokeAuthorizedSigner(msg.sender, signer);
}

/**
* @notice Initiate a payment collection through the payments protocol
Expand All @@ -58,43 +127,10 @@ contract TAPCollector is EIP712, GraphDirectory, ITAPCollector {
TAPCollectorCallerNotDataService(msg.sender, signedRAV.rav.dataService)
);

address dataService = signedRAV.rav.dataService;
address payer = _recoverRAVSigner(signedRAV);
address receiver = signedRAV.rav.serviceProvider;
address signer = _recoverRAVSigner(signedRAV);
require(authorizedSigners[signer].payer != address(0), TAPCollectorInvalidRAVSigner());

uint256 tokensRAV = signedRAV.rav.valueAggregate;
uint256 tokensAlreadyCollected = tokensCollected[dataService][receiver][payer];
require(
tokensRAV > tokensAlreadyCollected,
TAPCollectorInconsistentRAVTokens(tokensRAV, tokensAlreadyCollected)
);

uint256 tokensToCollect = tokensRAV - tokensAlreadyCollected;
uint256 tokensDataService = tokensToCollect.mulPPM(dataServiceCut);

if (tokensToCollect > 0) {
_graphPaymentsEscrow().collect(
paymentType,
payer,
receiver,
tokensToCollect,
dataService,
tokensDataService
);
tokensCollected[dataService][receiver][payer] = tokensRAV;
}

emit PaymentCollected(paymentType, payer, receiver, tokensToCollect, dataService, tokensDataService);
emit RAVCollected(
payer,
dataService,
receiver,
signedRAV.rav.timestampNs,
signedRAV.rav.valueAggregate,
signedRAV.rav.metadata,
signedRAV.signature
);
return tokensToCollect;
return _collect(paymentType, authorizedSigners[signer].payer, signedRAV, dataServiceCut);
}

/**
Expand Down Expand Up @@ -137,4 +173,74 @@ contract TAPCollector is EIP712, GraphDirectory, ITAPCollector {
)
);
}

/**
* @notice Verify the proof provided by the payer authorizing the signer
* @param proof The proof provided by the payer authorizing the signer
* @param proofDeadline The deadline by which the proof must be verified
* @param signer The signer to be authorized
*/
function verifyAuthorizedSignerProof(bytes calldata proof, uint256 proofDeadline, address signer) private view {
// Verify that the proofDeadline has not passed
require(
proofDeadline > block.timestamp,
TAPCollectorInvalidSignerProofDeadline(proofDeadline, block.timestamp)
);

// Generate the hash of the payer's address
bytes32 messageHash = keccak256(abi.encodePacked(block.chainid, proofDeadline, msg.sender));

// Generate the digest to be signed by the signer
bytes32 digest = MessageHashUtils.toEthSignedMessageHash(messageHash);

// Verify that the recovered signer matches the expected signer
require(ECDSA.recover(digest, proof) == signer, TAPCollectorInvalidSignerProof());
}

/**
* @notice See {ITAPCollector.collect}
*/
function _collect(
IGraphPayments.PaymentTypes paymentType,
address payer,
SignedRAV memory signedRAV,
uint256 dataServiceCut
) private returns (uint256) {
address dataService = signedRAV.rav.dataService;
address receiver = signedRAV.rav.serviceProvider;

uint256 tokensRAV = signedRAV.rav.valueAggregate;
uint256 tokensAlreadyCollected = tokensCollected[dataService][receiver][payer];
require(
tokensRAV > tokensAlreadyCollected,
TAPCollectorInconsistentRAVTokens(tokensRAV, tokensAlreadyCollected)
);

uint256 tokensToCollect = tokensRAV - tokensAlreadyCollected;
uint256 tokensDataService = tokensToCollect.mulPPM(dataServiceCut);

if (tokensToCollect > 0) {
_graphPaymentsEscrow().collect(
paymentType,
payer,
receiver,
tokensToCollect,
dataService,
tokensDataService
);
tokensCollected[dataService][receiver][payer] = tokensRAV;
}

emit PaymentCollected(paymentType, payer, receiver, tokensToCollect, dataService, tokensDataService);
emit RAVCollected(
payer,
dataService,
receiver,
signedRAV.rav.timestampNs,
signedRAV.rav.valueAggregate,
signedRAV.rav.metadata,
signedRAV.signature
);
return tokensToCollect;
}
}
2 changes: 1 addition & 1 deletion packages/horizon/test/GraphBase.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ abstract contract GraphBaseTest is IHorizonStakingTypes, Utils, Constants {
subgraphDataServiceLegacyAddress
);

tapCollector = new TAPCollector("TAPCollector", "1", address(controller));
tapCollector = new TAPCollector("TAPCollector", "1", address(controller), revokeSignerThawingPeriod);

resetPrank(users.governor);
proxyAdmin.upgrade(stakingProxy, address(stakingBase));
Expand Down
Loading
Loading