Skip to content
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
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 payer The address of the payer authorizing the signer
* @param authorizedSigner The address of the authorized signer
*/
event SignerAuthorized(address indexed payer, address indexed authorizedSigner);

/**
* @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 SignerThawing(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 SignerThawCanceled(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 SignerRevoked(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 authorizingPayer The address of the payer authorizing the signer
* @param signer The address of the signer
*/
error TAPCollectorSignerAlreadyAuthorized(address authorizingPayer, address signer);

/**
* 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 payer The address of the payer
* @param signer The address of the signer
*/
error TAPCollectorSignerNotAuthorizedByPayer(address payer, address signer);

/**
* 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 {SignerAuthorized} 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 {SignerThawing} 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 {SignerThawCanceled} 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 {SignerRevoked} 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
168 changes: 137 additions & 31 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 REVOKE_SIGNER_THAWING_PERIOD;

/**
* @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) {
REVOKE_SIGNER_THAWING_PERIOD = revokeSignerThawingPeriod;
}

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

_verifyAuthorizedSignerProof(proof, proofDeadline, signer);

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

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

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

authorization.thawEndTimestamp = block.timestamp + REVOKE_SIGNER_THAWING_PERIOD;
emit SignerThawing(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(msg.sender, signer));
require(authorization.thawEndTimestamp > 0, TAPCollectorSignerNotThawing(signer));

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

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

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

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

/**
* @notice Initiate a payment collection through the payments protocol
Expand All @@ -58,59 +127,73 @@ 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());

return _collect(paymentType, authorizedSigners[signer].payer, signedRAV, dataServiceCut);
}

/**
* @notice See {ITAPCollector.recoverRAVSigner}
*/
function recoverRAVSigner(SignedRAV calldata signedRAV) external view override returns (address) {
return _recoverRAVSigner(signedRAV);
}

/**
* @notice See {ITAPCollector.encodeRAV}
*/
function encodeRAV(ReceiptAggregateVoucher calldata rav) external view returns (bytes32) {
return _encodeRAV(rav);
}

uint256 tokensRAV = signedRAV.rav.valueAggregate;
uint256 tokensAlreadyCollected = tokensCollected[dataService][receiver][payer];
/**
* @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);
uint256 tokensDataService = tokensToCollect.mulPPM(_dataServiceCut);

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

emit PaymentCollected(paymentType, payer, receiver, tokensToCollect, dataService, tokensDataService);
emit PaymentCollected(_paymentType, _payer, receiver, tokensToCollect, dataService, tokensDataService);
emit RAVCollected(
payer,
_payer,
dataService,
receiver,
signedRAV.rav.timestampNs,
signedRAV.rav.valueAggregate,
signedRAV.rav.metadata,
signedRAV.signature
_signedRAV.rav.timestampNs,
_signedRAV.rav.valueAggregate,
_signedRAV.rav.metadata,
_signedRAV.signature
);
return tokensToCollect;
}

/**
* @notice See {ITAPCollector.recoverRAVSigner}
*/
function recoverRAVSigner(SignedRAV calldata signedRAV) external view override returns (address) {
return _recoverRAVSigner(signedRAV);
}

/**
* @notice See {ITAPCollector.encodeRAV}
*/
function encodeRAV(ReceiptAggregateVoucher calldata rav) external view returns (bytes32) {
return _encodeRAV(rav);
}

/**
* @notice See {ITAPCollector.recoverRAVSigner}
*/
Expand All @@ -137,4 +220,27 @@ 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());
}
}
Loading