Skip to content

Commit

Permalink
chore(Horizon): add signers to TAPCollector (#1060)
Browse files Browse the repository at this point in the history
* chore(Horizon): add signers to TAPCollector

* fix: collect multiple queries test

* fix: rename events and change parameter order to make it consistent

* fix: lint issues

* chore: add tap collector signer unit tests
  • Loading branch information
Maikol authored Oct 11, 2024
1 parent 971a5c5 commit 504fff1
Show file tree
Hide file tree
Showing 18 changed files with 827 additions and 215 deletions.
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

0 comments on commit 504fff1

Please sign in to comment.