diff --git a/packages/horizon/contracts/interfaces/IAuthorizable.sol b/packages/horizon/contracts/interfaces/IAuthorizable.sol new file mode 100644 index 000000000..41500fc17 --- /dev/null +++ b/packages/horizon/contracts/interfaces/IAuthorizable.sol @@ -0,0 +1,150 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.27; + +/** + * @title Interface for the {Authorizable} contract + * @notice Implements an authorization scheme that allows authorizers to + * authorize signers to sign on their behalf. + */ +interface IAuthorizable { + /** + * @notice Details for an authorizer-signer pair + * @dev Authorizations can be removed only after a thawing period + */ + struct Authorization { + // Resource owner + address authorizer; + // Timestamp at which thawing period ends (zero if not thawing) + uint256 thawEndTimestamp; + // Whether the signer authorization was revoked + bool revoked; + } + + /** + * @notice Emitted when a signer is authorized to sign for a authorizer + * @param authorizer The address of the authorizer + * @param signer The address of the signer + */ + event SignerAuthorized(address indexed authorizer, address indexed signer); + + /** + * @notice Emitted when a signer is thawed to be de-authorized + * @param authorizer The address of the authorizer thawing the signer + * @param signer The address of the signer to thaw + * @param thawEndTimestamp The timestamp at which the thawing period ends + */ + event SignerThawing(address indexed authorizer, address indexed signer, uint256 thawEndTimestamp); + + /** + * @dev Emitted when the thawing of a signer is cancelled + * @param authorizer The address of the authorizer cancelling the thawing + * @param signer The address of the signer + * @param thawEndTimestamp The timestamp at which the thawing period was scheduled to end + */ + event SignerThawCanceled(address indexed authorizer, address indexed signer, uint256 thawEndTimestamp); + + /** + * @dev Emitted when a signer has been revoked after thawing + * @param authorizer The address of the authorizer revoking the signer + * @param signer The address of the signer + */ + event SignerRevoked(address indexed authorizer, address indexed signer); + + /** + * Thrown when attempting to authorize a signer that is already authorized + * @param authorizer The address of the authorizer + * @param signer The address of the signer + * @param revoked The revoked status of the authorization + */ + error AuthorizableSignerAlreadyAuthorized(address authorizer, address signer, bool revoked); + + /** + * Thrown when the signer proof deadline is invalid + * @param proofDeadline The deadline for the proof provided + * @param currentTimestamp The current timestamp + */ + error AuthorizableInvalidSignerProofDeadline(uint256 proofDeadline, uint256 currentTimestamp); + + /** + * Thrown when the signer proof is invalid + */ + error AuthorizableInvalidSignerProof(); + + /** + * Thrown when the signer is not authorized by the authorizer + * @param authorizer The address of the authorizer + * @param signer The address of the signer + */ + error AuthorizableSignerNotAuthorized(address authorizer, address signer); + + /** + * Thrown when the signer is not thawing + * @param signer The address of the signer + */ + error AuthorizableSignerNotThawing(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 AuthorizableSignerStillThawing(uint256 currentTimestamp, uint256 thawEndTimestamp); + + /** + * @notice Authorize a signer to sign on behalf of the authorizer + * @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 a {SignerAuthorized} event + * @param signer The addres of the 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 authorizer + * consists of (chain id, verifying contract address, domain, proof deadline, authorizer address) + */ + function authorizeSigner(address signer, uint256 proofDeadline, bytes calldata proof) external; + + /** + * @notice Starts thawing a signer to be de-authorized + * @dev Thawing a signer signals that signatures from that signer will soon be deemed invalid. + * Once a signer is thawed, they should be viewed as revoked regardless of their revocation status. + * If a signer is already thawing and this function is called, the thawing period is reset. + * Requirements: + * - `signer` must be authorized by the authorizer 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 function caller + * + * Emits a {SignerThawCanceled} event + * @param signer The address of the signer to cancel thawing + */ + function cancelThawSigner(address signer) external; + + /** + * @notice Revokes a signer if thawed. + * @dev Requirements: + * - `signer` must be thawed and authorized by the function caller + * + * Emits a {SignerRevoked} event + * @param signer The address of the signer + */ + function revokeAuthorizedSigner(address signer) external; + + /** + * @notice Returns the timestamp at which the thawing period ends for a signer + */ + function getThawEnd(address signer) external view returns (uint256); + + /** + * @notice Returns true if the signer is authorized by the authorizer + */ + function isAuthorized(address authorizer, address signer) external view returns (bool); +} diff --git a/packages/horizon/contracts/interfaces/IGraphTallyCollector.sol b/packages/horizon/contracts/interfaces/IGraphTallyCollector.sol index 52cdb8647..2376d59eb 100644 --- a/packages/horizon/contracts/interfaces/IGraphTallyCollector.sol +++ b/packages/horizon/contracts/interfaces/IGraphTallyCollector.sol @@ -12,17 +12,6 @@ import { IGraphPayments } from "./IGraphPayments.sol"; * payments using a GraphTally RAV (Receipt Aggregate Voucher). */ interface IGraphTallyCollector 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; - // Whether the signer authorization was revoked - bool revoked; - } - /// @notice The Receipt Aggregate Voucher (RAV) struct struct ReceiptAggregateVoucher { // The ID of the collection "bucket" the RAV belongs to. Note that multiple RAVs can be collected for the same collection id. @@ -50,36 +39,6 @@ interface IGraphTallyCollector 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 collectionId The ID of the collection "bucket" the RAV belongs to. @@ -102,70 +61,11 @@ interface IGraphTallyCollector 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 GraphTallyCollectorSignerAlreadyAuthorized(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 GraphTallyCollectorInvalidSignerProofDeadline(uint256 proofDeadline, uint256 currentTimestamp); - - /** - * Thrown when the signer proof is invalid - */ - error GraphTallyCollectorInvalidSignerProof(); - - /** - * 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 GraphTallyCollectorSignerNotAuthorizedByPayer(address payer, address signer); - - /** - * Thrown when the attempting to revoke a signer that was already revoked - * @param signer The address of the signer - */ - error GraphTallyCollectorAuthorizationAlreadyRevoked(address payer, address signer); - - /** - * Thrown when attempting to thaw a signer that is already thawing - * @param signer The address of the signer - * @param thawEndTimestamp The timestamp at which the thawing period ends - */ - error GraphTallyCollectorSignerAlreadyThawing(address signer, uint256 thawEndTimestamp); - - /** - * Thrown when the signer is not thawing - * @param signer The address of the signer - */ - error GraphTallyCollectorSignerNotThawing(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 GraphTallyCollectorSignerStillThawing(uint256 currentTimestamp, uint256 thawEndTimestamp); - /** * Thrown when the RAV signer is invalid */ error GraphTallyCollectorInvalidRAVSigner(); - /** - * Thrown when the RAV payer does not match the signers authorized payer - * @param authorizedPayer The address of the authorized payer - * @param ravPayer The address of the RAV payer - */ - error GraphTallyCollectorInvalidRAVPayer(address authorizedPayer, address ravPayer); - /** * Thrown when the RAV is for a data service the service provider has no provision for * @param dataService The address of the data service @@ -194,55 +94,6 @@ interface IGraphTallyCollector is IPaymentsCollector { */ error GraphTallyCollectorInvalidTokensToCollectAmount(uint256 tokensToCollect, uint256 maxTokensToCollect); - /** - * @notice Authorize a signer to sign on behalf of the payer. - * A signer can not be authorized for multiple payers even after revoking previous authorizations. - * @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; - /** * @notice See {IPaymentsCollector.collect} * This variant adds the ability to partially collect a RAV by specifying the amount of tokens to collect. diff --git a/packages/horizon/contracts/mocks/ControllerMock.sol b/packages/horizon/contracts/mocks/ControllerMock.sol index 557b1eff6..54c3ec8db 100644 --- a/packages/horizon/contracts/mocks/ControllerMock.sol +++ b/packages/horizon/contracts/mocks/ControllerMock.sol @@ -103,7 +103,7 @@ contract ControllerMock is IController { * @param id Contract id (keccak256 hash of contract name) * @return Address of the proxy contract for the provided id */ - function getContractProxy(bytes32 id) external view override returns (address) { + function getContractProxy(bytes32 id) external view virtual override returns (address) { return _registry[id]; } diff --git a/packages/horizon/contracts/payments/collectors/GraphTallyCollector.sol b/packages/horizon/contracts/payments/collectors/GraphTallyCollector.sol index b9c3c9df9..6ccfc2a0c 100644 --- a/packages/horizon/contracts/payments/collectors/GraphTallyCollector.sol +++ b/packages/horizon/contracts/payments/collectors/GraphTallyCollector.sol @@ -4,26 +4,24 @@ pragma solidity 0.8.27; import { IGraphPayments } from "../../interfaces/IGraphPayments.sol"; import { IGraphTallyCollector } from "../../interfaces/IGraphTallyCollector.sol"; +import { Authorizable } from "../../utilities/Authorizable.sol"; import { EIP712 } from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; 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 GraphTallyCollector contract - * @dev Implements the {IGraphTallyCollector} and {IPaymentCollector} interfaces. + * @dev Implements the {IGraphTallyCollector}, {IPaymentCollector} and {IAuthorizable} interfaces. * @notice A payments collector contract that can be used to collect payments using a GraphTally RAV (Receipt Aggregate Voucher). * @dev Note that the contract expects the RAV aggregate value to be monotonically increasing, each successive RAV for the same * (data service-payer-receiver) tuple should have a value greater than the previous one. The contract will keep track of the tokens * already collected and calculate the difference to collect. - * @dev The contract also implements a mechanism to authorize signers to sign RAVs on behalf of a payer. Signers cannot be reused - * for different payers. * @custom:security-contact Please email security+contracts@thegraph.com if you find any * bugs. We may have an active bug bounty program. */ -contract GraphTallyCollector is EIP712, GraphDirectory, IGraphTallyCollector { +contract GraphTallyCollector is EIP712, GraphDirectory, Authorizable, IGraphTallyCollector { using PPMMath for uint256; /// @notice The EIP712 typehash for the ReceiptAggregateVoucher struct @@ -32,18 +30,12 @@ contract GraphTallyCollector is EIP712, GraphDirectory, IGraphTallyCollector { "ReceiptAggregateVoucher(address payer,address serviceProvider,address dataService,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. /// @dev The collectionId provides a secondary key for grouping payment tracking if needed. Data services that do not require /// grouping can use the same collectionId for all payments (0x00 or some other default value). mapping(address dataService => mapping(bytes32 collectionId => 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 GraphTallyCollector contract. * @param eip712Name The name of the EIP712 domain. @@ -56,72 +48,7 @@ contract GraphTallyCollector is EIP712, GraphDirectory, IGraphTallyCollector { string memory eip712Version, address controller, uint256 revokeSignerThawingPeriod - ) EIP712(eip712Name, eip712Version) GraphDirectory(controller) { - REVOKE_SIGNER_THAWING_PERIOD = revokeSignerThawingPeriod; - } - - /** - * See {IGraphTallyCollector.authorizeSigner}. - */ - function authorizeSigner(address signer, uint256 proofDeadline, bytes calldata proof) external override { - require( - authorizedSigners[signer].payer == address(0), - GraphTallyCollectorSignerAlreadyAuthorized(authorizedSigners[signer].payer, signer) - ); - - _verifyAuthorizedSignerProof(proof, proofDeadline, signer); - - authorizedSigners[signer].payer = msg.sender; - authorizedSigners[signer].thawEndTimestamp = 0; - emit SignerAuthorized(msg.sender, signer); - } - - /** - * See {IGraphTallyCollector.thawSigner}. - */ - function thawSigner(address signer) external override { - PayerAuthorization storage authorization = authorizedSigners[signer]; - - require(authorization.payer == msg.sender, GraphTallyCollectorSignerNotAuthorizedByPayer(msg.sender, signer)); - require(!authorization.revoked, GraphTallyCollectorAuthorizationAlreadyRevoked(msg.sender, signer)); - require( - authorization.thawEndTimestamp == 0, - GraphTallyCollectorSignerAlreadyThawing(signer, authorization.thawEndTimestamp) - ); - - authorization.thawEndTimestamp = block.timestamp + REVOKE_SIGNER_THAWING_PERIOD; - emit SignerThawing(msg.sender, signer, authorization.thawEndTimestamp); - } - - /** - * See {IGraphTallyCollector.cancelThawSigner}. - */ - function cancelThawSigner(address signer) external override { - PayerAuthorization storage authorization = authorizedSigners[signer]; - - require(authorization.payer == msg.sender, GraphTallyCollectorSignerNotAuthorizedByPayer(msg.sender, signer)); - require(authorization.thawEndTimestamp > 0, GraphTallyCollectorSignerNotThawing(signer)); - - authorization.thawEndTimestamp = 0; - emit SignerThawCanceled(msg.sender, signer, 0); - } - - /** - * See {IGraphTallyCollector.revokeAuthorizedSigner}. - */ - function revokeAuthorizedSigner(address signer) external override { - PayerAuthorization storage authorization = authorizedSigners[signer]; - - require(authorization.payer == msg.sender, GraphTallyCollectorSignerNotAuthorizedByPayer(msg.sender, signer)); - require(authorization.thawEndTimestamp > 0, GraphTallyCollectorSignerNotThawing(signer)); - require( - authorization.thawEndTimestamp <= block.timestamp, - GraphTallyCollectorSignerStillThawing(block.timestamp, authorization.thawEndTimestamp) - ); - - authorization.revoked = true; - emit SignerRevoked(msg.sender, signer); - } + ) EIP712(eip712Name, eip712Version) GraphDirectory(controller) Authorizable(revokeSignerThawingPeriod) {} /** * @notice Initiate a payment collection through the payments protocol @@ -174,16 +101,8 @@ contract GraphTallyCollector is EIP712, GraphDirectory, IGraphTallyCollector { GraphTallyCollectorCallerNotDataService(msg.sender, signedRAV.rav.dataService) ); - // Ensure RAV signer is authorized for a payer - address signer = _recoverRAVSigner(signedRAV); - require( - authorizedSigners[signer].payer != address(0) && !authorizedSigners[signer].revoked, - GraphTallyCollectorInvalidRAVSigner() - ); - - // Ensure RAV payer matches the authorized payer - address payer = authorizedSigners[signer].payer; - require(signedRAV.rav.payer == payer, GraphTallyCollectorInvalidRAVPayer(payer, signedRAV.rav.payer)); + // Ensure RAV signer is authorized for the payer + _requireAuthorizedSigner(signedRAV); bytes32 collectionId = signedRAV.rav.collectionId; address dataService = signedRAV.rav.dataService; @@ -201,6 +120,7 @@ contract GraphTallyCollector is EIP712, GraphDirectory, IGraphTallyCollector { } uint256 tokensToCollect = 0; + address payer = signedRAV.rav.payer; { uint256 tokensRAV = signedRAV.rav.valueAggregate; uint256 tokensAlreadyCollected = tokensCollected[dataService][collectionId][receiver][payer]; @@ -273,26 +193,10 @@ contract GraphTallyCollector is EIP712, GraphDirectory, IGraphTallyCollector { ); } - /** - * @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 + function _requireAuthorizedSigner(SignedRAV memory _signedRAV) private view { require( - _proofDeadline > block.timestamp, - GraphTallyCollectorInvalidSignerProofDeadline(_proofDeadline, block.timestamp) + _isAuthorized(_signedRAV.rav.payer, _recoverRAVSigner(_signedRAV)), + GraphTallyCollectorInvalidRAVSigner() ); - - // Generate the hash of the payer's address - bytes32 messageHash = keccak256(abi.encodePacked(block.chainid, address(this), _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, GraphTallyCollectorInvalidSignerProof()); } } diff --git a/packages/horizon/contracts/utilities/Authorizable.sol b/packages/horizon/contracts/utilities/Authorizable.sol new file mode 100644 index 000000000..0296bf033 --- /dev/null +++ b/packages/horizon/contracts/utilities/Authorizable.sol @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.27; + +import { IAuthorizable } from "../interfaces/IAuthorizable.sol"; + +import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import { MessageHashUtils } from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; + +/** + * @title Authorizable contract + * @dev Implements the {IAuthorizable} interface. + * @notice A mechanism to authorize signers to sign messages on behalf of an authorizer. + * Signers cannot be reused for different authorizers. + * @dev Contract uses "authorizeSignerProof" as the domain for signer proofs. + * @custom:security-contact Please email security+contracts@thegraph.com if you find any + * bugs. We may have an active bug bounty program. + */ +abstract contract Authorizable is IAuthorizable { + /// @notice The duration (in seconds) for which an authorization is thawing before it can be revoked + uint256 public immutable REVOKE_AUTHORIZATION_THAWING_PERIOD; + + /// @notice Authorization details for authorizer-signer pairs + mapping(address signer => Authorization authorization) public authorizations; + + /** + * @dev Revert if the caller has not authorized the signer + */ + modifier onlyAuthorized(address signer) { + _requireAuthorized(msg.sender, signer); + _; + } + + /** + * @notice Constructs a new instance of the Authorizable contract. + * @param revokeAuthorizationThawingPeriod The duration (in seconds) for which an authorization is thawing before it can be revoked. + */ + constructor(uint256 revokeAuthorizationThawingPeriod) { + REVOKE_AUTHORIZATION_THAWING_PERIOD = revokeAuthorizationThawingPeriod; + } + + /** + * See {IAuthorizable.authorizeSigner}. + */ + function authorizeSigner(address signer, uint256 proofDeadline, bytes calldata proof) external { + require( + authorizations[signer].authorizer == address(0), + AuthorizableSignerAlreadyAuthorized( + authorizations[signer].authorizer, + signer, + authorizations[signer].revoked + ) + ); + _verifyAuthorizationProof(proof, proofDeadline, signer); + authorizations[signer].authorizer = msg.sender; + emit SignerAuthorized(msg.sender, signer); + } + + /** + * See {IAuthorizable.thawSigner}. + */ + function thawSigner(address signer) external onlyAuthorized(signer) { + authorizations[signer].thawEndTimestamp = block.timestamp + REVOKE_AUTHORIZATION_THAWING_PERIOD; + emit SignerThawing(msg.sender, signer, authorizations[signer].thawEndTimestamp); + } + + /** + * See {IAuthorizable.cancelThawSigner}. + */ + function cancelThawSigner(address signer) external onlyAuthorized(signer) { + require(authorizations[signer].thawEndTimestamp > 0, AuthorizableSignerNotThawing(signer)); + uint256 thawEnd = authorizations[signer].thawEndTimestamp; + authorizations[signer].thawEndTimestamp = 0; + emit SignerThawCanceled(msg.sender, signer, thawEnd); + } + + /** + * See {IAuthorizable.revokeAuthorizedSigner}. + */ + function revokeAuthorizedSigner(address signer) external onlyAuthorized(signer) { + uint256 thawEndTimestamp = authorizations[signer].thawEndTimestamp; + require(thawEndTimestamp > 0, AuthorizableSignerNotThawing(signer)); + require(thawEndTimestamp <= block.timestamp, AuthorizableSignerStillThawing(block.timestamp, thawEndTimestamp)); + authorizations[signer].revoked = true; + emit SignerRevoked(msg.sender, signer); + } + + /** + * See {IAuthorizable.getThawEnd}. + */ + function getThawEnd(address signer) external view returns (uint256) { + return authorizations[signer].thawEndTimestamp; + } + + /** + * See {IAuthorizable.isAuthorized}. + */ + function isAuthorized(address authorizer, address signer) external view returns (bool) { + return _isAuthorized(authorizer, signer); + } + + /** + * See {IAuthorizable.isAuthorized}. + */ + function _isAuthorized(address _authorizer, address _signer) internal view returns (bool) { + return (_authorizer != address(0) && + authorizations[_signer].authorizer == _authorizer && + !authorizations[_signer].revoked); + } + + /** + * @notice Reverts if the authorizer has not authorized the signer + * @param _authorizer The address of the authorizer + * @param _signer The address of the signer + */ + function _requireAuthorized(address _authorizer, address _signer) internal view { + require(_isAuthorized(_authorizer, _signer), AuthorizableSignerNotAuthorized(_authorizer, _signer)); + } + + /** + * @notice Verify the authorization proof provided by the authorizer + * @param _proof The proof provided by the authorizer + * @param _proofDeadline The deadline by which the proof must be verified + * @param _signer The authorization recipient + */ + function _verifyAuthorizationProof(bytes calldata _proof, uint256 _proofDeadline, address _signer) private view { + // Check that the proofDeadline has not passed + require( + _proofDeadline > block.timestamp, + AuthorizableInvalidSignerProofDeadline(_proofDeadline, block.timestamp) + ); + + // Generate the message hash + bytes32 messageHash = keccak256( + abi.encodePacked(block.chainid, address(this), "authorizeSignerProof", _proofDeadline, msg.sender) + ); + + // Generate the allegedly signed digest + bytes32 digest = MessageHashUtils.toEthSignedMessageHash(messageHash); + + // Verify that the recovered signer matches the to be authorized signer + require(ECDSA.recover(digest, _proof) == _signer, AuthorizableInvalidSignerProof()); + } +} diff --git a/packages/horizon/test/payments/graph-tally-collector/GraphTallyCollector.t.sol b/packages/horizon/test/payments/graph-tally-collector/GraphTallyCollector.t.sol index 6b29153de..4d0bb4233 100644 --- a/packages/horizon/test/payments/graph-tally-collector/GraphTallyCollector.t.sol +++ b/packages/horizon/test/payments/graph-tally-collector/GraphTallyCollector.t.sol @@ -9,6 +9,7 @@ import { IHorizonStakingMain } from "../../../contracts/interfaces/internal/IHor import { IGraphTallyCollector } from "../../../contracts/interfaces/IGraphTallyCollector.sol"; import { IPaymentsCollector } from "../../../contracts/interfaces/IPaymentsCollector.sol"; import { IGraphPayments } from "../../../contracts/interfaces/IGraphPayments.sol"; +import { IAuthorizable } from "../../../contracts/interfaces/IAuthorizable.sol"; import { GraphTallyCollector } from "../../../contracts/payments/collectors/GraphTallyCollector.sol"; import { PPMMath } from "../../../contracts/libraries/PPMMath.sol"; @@ -49,7 +50,13 @@ contract GraphTallyTest is HorizonStakingSharedTest, PaymentsEscrowSharedTest { function _getSignerProof(uint256 _proofDeadline, uint256 _signer) internal returns (bytes memory) { (, address msgSender, ) = vm.readCallers(); bytes32 messageHash = keccak256( - abi.encodePacked(block.chainid, address(graphTallyCollector), _proofDeadline, msgSender) + abi.encodePacked( + block.chainid, + address(graphTallyCollector), + "authorizeSignerProof", + _proofDeadline, + msgSender + ) ); bytes32 proofToDigest = MessageHashUtils.toEthSignedMessageHash(messageHash); (uint8 v, bytes32 r, bytes32 s) = vm.sign(_signer, proofToDigest); @@ -64,14 +71,11 @@ contract GraphTallyTest is HorizonStakingSharedTest, PaymentsEscrowSharedTest { (, address msgSender, ) = vm.readCallers(); vm.expectEmit(address(graphTallyCollector)); - emit IGraphTallyCollector.SignerAuthorized(msgSender, _signer); + emit IAuthorizable.SignerAuthorized(msgSender, _signer); graphTallyCollector.authorizeSigner(_signer, _proofDeadline, _proof); - - (address _payer, uint256 thawEndTimestamp, bool revoked) = graphTallyCollector.authorizedSigners(_signer); - assertEq(_payer, msgSender); - assertEq(thawEndTimestamp, 0); - assertEq(revoked, false); + assertTrue(graphTallyCollector.isAuthorized(msgSender, _signer)); + assertEq(graphTallyCollector.getThawEnd(_signer), 0); } function _thawSigner(address _signer) internal { @@ -79,47 +83,38 @@ contract GraphTallyTest is HorizonStakingSharedTest, PaymentsEscrowSharedTest { uint256 expectedThawEndTimestamp = block.timestamp + revokeSignerThawingPeriod; vm.expectEmit(address(graphTallyCollector)); - emit IGraphTallyCollector.SignerThawing(msgSender, _signer, expectedThawEndTimestamp); + emit IAuthorizable.SignerThawing(msgSender, _signer, expectedThawEndTimestamp); graphTallyCollector.thawSigner(_signer); - (address _payer, uint256 thawEndTimestamp, bool revoked) = graphTallyCollector.authorizedSigners(_signer); - assertEq(_payer, msgSender); - assertEq(thawEndTimestamp, expectedThawEndTimestamp); - assertEq(revoked, false); + assertTrue(graphTallyCollector.isAuthorized(msgSender, _signer)); + assertEq(graphTallyCollector.getThawEnd(_signer), expectedThawEndTimestamp); } function _cancelThawSigner(address _signer) internal { (, address msgSender, ) = vm.readCallers(); vm.expectEmit(address(graphTallyCollector)); - emit IGraphTallyCollector.SignerThawCanceled(msgSender, _signer, 0); + emit IAuthorizable.SignerThawCanceled(msgSender, _signer, graphTallyCollector.getThawEnd(_signer)); graphTallyCollector.cancelThawSigner(_signer); - (address _payer, uint256 thawEndTimestamp, bool revoked) = graphTallyCollector.authorizedSigners(_signer); - assertEq(_payer, msgSender); - assertEq(thawEndTimestamp, 0); - assertEq(revoked, false); + assertTrue(graphTallyCollector.isAuthorized(msgSender, _signer)); + assertEq(graphTallyCollector.getThawEnd(_signer), 0); } function _revokeAuthorizedSigner(address _signer) internal { (, address msgSender, ) = vm.readCallers(); - (address beforePayer, uint256 beforeThawEndTimestamp, ) = graphTallyCollector.authorizedSigners(_signer); + assertTrue(graphTallyCollector.isAuthorized(msgSender, _signer)); + assertLt(graphTallyCollector.getThawEnd(_signer), block.timestamp); vm.expectEmit(address(graphTallyCollector)); - emit IGraphTallyCollector.SignerRevoked(msgSender, _signer); + emit IAuthorizable.SignerRevoked(msgSender, _signer); graphTallyCollector.revokeAuthorizedSigner(_signer); - (address afterPayer, uint256 afterThawEndTimestamp, bool afterRevoked) = graphTallyCollector.authorizedSigners( - _signer - ); - - assertEq(beforePayer, afterPayer); - assertEq(beforeThawEndTimestamp, afterThawEndTimestamp); - assertEq(afterRevoked, true); + assertFalse(graphTallyCollector.isAuthorized(msgSender, _signer)); } function _collect(IGraphPayments.PaymentTypes _paymentType, bytes memory _data) internal { @@ -139,14 +134,11 @@ contract GraphTallyTest is HorizonStakingSharedTest, PaymentsEscrowSharedTest { _data, (IGraphTallyCollector.SignedRAV, uint256) ); - bytes32 messageHash = graphTallyCollector.encodeRAV(signedRAV.rav); - address _signer = ECDSA.recover(messageHash, signedRAV.signature); - (address _payer, , ) = graphTallyCollector.authorizedSigners(_signer); uint256 tokensAlreadyCollected = graphTallyCollector.tokensCollected( signedRAV.rav.dataService, signedRAV.rav.collectionId, signedRAV.rav.serviceProvider, - _payer + signedRAV.rav.payer ); uint256 tokensToCollect = _tokensToCollect == 0 ? signedRAV.rav.valueAggregate - tokensAlreadyCollected @@ -156,7 +148,7 @@ contract GraphTallyTest is HorizonStakingSharedTest, PaymentsEscrowSharedTest { emit IPaymentsCollector.PaymentCollected( _paymentType, signedRAV.rav.collectionId, - _payer, + signedRAV.rav.payer, signedRAV.rav.serviceProvider, signedRAV.rav.dataService, tokensToCollect @@ -164,7 +156,7 @@ contract GraphTallyTest is HorizonStakingSharedTest, PaymentsEscrowSharedTest { vm.expectEmit(address(graphTallyCollector)); emit IGraphTallyCollector.RAVCollected( signedRAV.rav.collectionId, - _payer, + signedRAV.rav.payer, signedRAV.rav.serviceProvider, signedRAV.rav.dataService, signedRAV.rav.timestampNs, @@ -180,7 +172,7 @@ contract GraphTallyTest is HorizonStakingSharedTest, PaymentsEscrowSharedTest { signedRAV.rav.dataService, signedRAV.rav.collectionId, signedRAV.rav.serviceProvider, - _payer + signedRAV.rav.payer ); assertEq(tokensCollected, tokensToCollect); assertEq( diff --git a/packages/horizon/test/payments/graph-tally-collector/collect/collect.t.sol b/packages/horizon/test/payments/graph-tally-collector/collect/collect.t.sol index d59de2a1f..364536fbb 100644 --- a/packages/horizon/test/payments/graph-tally-collector/collect/collect.t.sol +++ b/packages/horizon/test/payments/graph-tally-collector/collect/collect.t.sol @@ -242,12 +242,7 @@ contract GraphTallyCollectTest is GraphTallyTest { bytes memory data = _getQueryFeeEncodedData(signerPrivateKey, params); resetPrank(users.verifier); - bytes memory expectedError = abi.encodeWithSelector( - IGraphTallyCollector.GraphTallyCollectorInvalidRAVPayer.selector, - users.gateway, - anotherPayer - ); - vm.expectRevert(expectedError); + vm.expectRevert(IGraphTallyCollector.GraphTallyCollectorInvalidRAVSigner.selector); graphTallyCollector.collect(IGraphPayments.PaymentTypes.QueryFee, data); } diff --git a/packages/horizon/test/payments/graph-tally-collector/signer/authorizeSigner.t.sol b/packages/horizon/test/payments/graph-tally-collector/signer/authorizeSigner.t.sol index 2cec32716..ecdef7004 100644 --- a/packages/horizon/test/payments/graph-tally-collector/signer/authorizeSigner.t.sol +++ b/packages/horizon/test/payments/graph-tally-collector/signer/authorizeSigner.t.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.27; import "forge-std/Test.sol"; -import { IGraphTallyCollector } from "../../../../contracts/interfaces/IGraphTallyCollector.sol"; +import { IAuthorizable } from "../../../../contracts/interfaces/IAuthorizable.sol"; import { GraphTallyTest } from "../GraphTallyCollector.t.sol"; @@ -27,10 +27,7 @@ contract GraphTallyAuthorizeSignerTest is GraphTallyTest { bytes memory signerProof = _getSignerProof(proofDeadline, signerPrivateKey); // Attempt to authorize delegator with payer's proof - bytes memory expectedError = abi.encodeWithSelector( - IGraphTallyCollector.GraphTallyCollectorInvalidSignerProof.selector - ); - vm.expectRevert(expectedError); + vm.expectRevert(IAuthorizable.AuthorizableInvalidSignerProof.selector); graphTallyCollector.authorizeSigner(users.delegator, proofDeadline, signerProof); } @@ -42,9 +39,10 @@ contract GraphTallyAuthorizeSignerTest is GraphTallyTest { // Attempt to authorize signer again bytes memory expectedError = abi.encodeWithSelector( - IGraphTallyCollector.GraphTallyCollectorSignerAlreadyAuthorized.selector, + IAuthorizable.AuthorizableSignerAlreadyAuthorized.selector, users.gateway, - signer + signer, + false ); vm.expectRevert(expectedError); graphTallyCollector.authorizeSigner(signer, proofDeadline, signerProof); @@ -55,7 +53,6 @@ contract GraphTallyAuthorizeSignerTest is GraphTallyTest { uint256 proofDeadline = block.timestamp + 1; bytes memory signerProof = _getSignerProof(proofDeadline, signerPrivateKey); _authorizeSigner(signer, proofDeadline, signerProof); - // Revoke signer _thawSigner(signer); skip(revokeSignerThawingPeriod + 1); @@ -63,9 +60,10 @@ contract GraphTallyAuthorizeSignerTest is GraphTallyTest { // Attempt to authorize signer again bytes memory expectedError = abi.encodeWithSelector( - IGraphTallyCollector.GraphTallyCollectorSignerAlreadyAuthorized.selector, + IAuthorizable.AuthorizableSignerAlreadyAuthorized.selector, users.gateway, - signer + signer, + true ); vm.expectRevert(expectedError); graphTallyCollector.authorizeSigner(signer, proofDeadline, signerProof); @@ -78,7 +76,7 @@ contract GraphTallyAuthorizeSignerTest is GraphTallyTest { // Attempt to authorize delegator with expired proof bytes memory expectedError = abi.encodeWithSelector( - IGraphTallyCollector.GraphTallyCollectorInvalidSignerProofDeadline.selector, + IAuthorizable.AuthorizableInvalidSignerProofDeadline.selector, proofDeadline, block.timestamp ); diff --git a/packages/horizon/test/payments/graph-tally-collector/signer/cancelThawSigner.t.sol b/packages/horizon/test/payments/graph-tally-collector/signer/cancelThawSigner.t.sol index aa741276d..460f47d46 100644 --- a/packages/horizon/test/payments/graph-tally-collector/signer/cancelThawSigner.t.sol +++ b/packages/horizon/test/payments/graph-tally-collector/signer/cancelThawSigner.t.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.27; import "forge-std/Test.sol"; -import { IGraphTallyCollector } from "../../../../contracts/interfaces/IGraphTallyCollector.sol"; +import { IAuthorizable } from "../../../../contracts/interfaces/IAuthorizable.sol"; import { GraphTallyTest } from "../GraphTallyCollector.t.sol"; @@ -19,7 +19,7 @@ contract GraphTallyCancelThawSignerTest is GraphTallyTest { function testGraphTally_CancelThawSigner_RevertWhen_NotAuthorized() public useGateway { bytes memory expectedError = abi.encodeWithSelector( - IGraphTallyCollector.GraphTallyCollectorSignerNotAuthorizedByPayer.selector, + IAuthorizable.AuthorizableSignerNotAuthorized.selector, users.gateway, signer ); @@ -29,7 +29,7 @@ contract GraphTallyCancelThawSignerTest is GraphTallyTest { function testGraphTally_CancelThawSigner_RevertWhen_NotThawing() public useGateway useSigner { bytes memory expectedError = abi.encodeWithSelector( - IGraphTallyCollector.GraphTallyCollectorSignerNotThawing.selector, + IAuthorizable.AuthorizableSignerNotThawing.selector, signer ); vm.expectRevert(expectedError); diff --git a/packages/horizon/test/payments/graph-tally-collector/signer/revokeSigner.t.sol b/packages/horizon/test/payments/graph-tally-collector/signer/revokeSigner.t.sol index d73438a37..ec32dfce8 100644 --- a/packages/horizon/test/payments/graph-tally-collector/signer/revokeSigner.t.sol +++ b/packages/horizon/test/payments/graph-tally-collector/signer/revokeSigner.t.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.27; import "forge-std/Test.sol"; -import { IGraphTallyCollector } from "../../../../contracts/interfaces/IGraphTallyCollector.sol"; +import { IAuthorizable } from "../../../../contracts/interfaces/IAuthorizable.sol"; import { GraphTallyTest } from "../GraphTallyCollector.t.sol"; @@ -23,7 +23,7 @@ contract GraphTallyRevokeAuthorizedSignerTest is GraphTallyTest { function testGraphTally_RevokeAuthorizedSigner_RevertWhen_NotAuthorized() public useGateway { bytes memory expectedError = abi.encodeWithSelector( - IGraphTallyCollector.GraphTallyCollectorSignerNotAuthorizedByPayer.selector, + IAuthorizable.AuthorizableSignerNotAuthorized.selector, users.gateway, signer ); @@ -33,7 +33,7 @@ contract GraphTallyRevokeAuthorizedSignerTest is GraphTallyTest { function testGraphTally_RevokeAuthorizedSigner_RevertWhen_NotThawing() public useGateway useSigner { bytes memory expectedError = abi.encodeWithSelector( - IGraphTallyCollector.GraphTallyCollectorSignerNotThawing.selector, + IAuthorizable.AuthorizableSignerNotThawing.selector, signer ); vm.expectRevert(expectedError); @@ -43,7 +43,7 @@ contract GraphTallyRevokeAuthorizedSignerTest is GraphTallyTest { function testGraphTally_RevokeAuthorizedSigner_RevertWhen_StillThawing() public useGateway useSigner { _thawSigner(signer); bytes memory expectedError = abi.encodeWithSelector( - IGraphTallyCollector.GraphTallyCollectorSignerStillThawing.selector, + IAuthorizable.AuthorizableSignerStillThawing.selector, block.timestamp, block.timestamp + revokeSignerThawingPeriod ); diff --git a/packages/horizon/test/payments/graph-tally-collector/signer/thawSigner.t.sol b/packages/horizon/test/payments/graph-tally-collector/signer/thawSigner.t.sol index 2d08512e7..49d5ace10 100644 --- a/packages/horizon/test/payments/graph-tally-collector/signer/thawSigner.t.sol +++ b/packages/horizon/test/payments/graph-tally-collector/signer/thawSigner.t.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.27; import "forge-std/Test.sol"; -import { IGraphTallyCollector } from "../../../../contracts/interfaces/IGraphTallyCollector.sol"; +import { IAuthorizable } from "../../../../contracts/interfaces/IAuthorizable.sol"; import { GraphTallyTest } from "../GraphTallyCollector.t.sol"; @@ -18,7 +18,7 @@ contract GraphTallyThawSignerTest is GraphTallyTest { function testGraphTally_ThawSigner_RevertWhen_NotAuthorized() public useGateway { bytes memory expectedError = abi.encodeWithSelector( - IGraphTallyCollector.GraphTallyCollectorSignerNotAuthorizedByPayer.selector, + IAuthorizable.AuthorizableSignerNotAuthorized.selector, users.gateway, signer ); @@ -32,7 +32,7 @@ contract GraphTallyThawSignerTest is GraphTallyTest { _revokeAuthorizedSigner(signer); bytes memory expectedError = abi.encodeWithSelector( - IGraphTallyCollector.GraphTallyCollectorAuthorizationAlreadyRevoked.selector, + IAuthorizable.AuthorizableSignerNotAuthorized.selector, users.gateway, signer ); @@ -40,16 +40,14 @@ contract GraphTallyThawSignerTest is GraphTallyTest { graphTallyCollector.thawSigner(signer); } - function testGraphTally_ThawSigner_RevertWhen_AlreadyThawing() public useGateway useSigner { + function testGraphTally_ThawSigner_AlreadyThawing() public useGateway useSigner { _thawSigner(signer); + uint256 originalThawEnd = graphTallyCollector.getThawEnd(signer); + skip(1); - (, uint256 thawEndTimestamp, ) = graphTallyCollector.authorizedSigners(signer); - bytes memory expectedError = abi.encodeWithSelector( - IGraphTallyCollector.GraphTallyCollectorSignerAlreadyThawing.selector, - signer, - thawEndTimestamp - ); - vm.expectRevert(expectedError); graphTallyCollector.thawSigner(signer); + uint256 currentThawEnd = graphTallyCollector.getThawEnd(signer); + vm.assertEq(originalThawEnd, block.timestamp + revokeSignerThawingPeriod - 1); + vm.assertEq(currentThawEnd, block.timestamp + revokeSignerThawingPeriod); } } diff --git a/packages/horizon/test/staking/provision/parameters.t.sol b/packages/horizon/test/staking/provision/parameters.t.sol index 589dd4fc3..6e0b4fabf 100644 --- a/packages/horizon/test/staking/provision/parameters.t.sol +++ b/packages/horizon/test/staking/provision/parameters.t.sol @@ -82,7 +82,6 @@ contract HorizonStakingProvisionParametersTest is HorizonStakingTest { vm.stopPrank(); } - function test_ProvisionParametersAccept_SameParameters( uint256 amount, uint32 maxVerifierCut, diff --git a/packages/horizon/test/utilities/Authorizable.t.sol b/packages/horizon/test/utilities/Authorizable.t.sol new file mode 100644 index 000000000..c31721af8 --- /dev/null +++ b/packages/horizon/test/utilities/Authorizable.t.sol @@ -0,0 +1,404 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.27; + +import { Test } from "forge-std/Test.sol"; + +import { Authorizable } from "../../contracts/utilities/Authorizable.sol"; +import { IAuthorizable } from "../../contracts/interfaces/IAuthorizable.sol"; +import { Bounder } from "../utils/Bounder.t.sol"; + +import { MessageHashUtils } from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; + +contract AuthorizableImp is Authorizable { + constructor(uint256 _revokeAuthorizationThawingPeriod) Authorizable(_revokeAuthorizationThawingPeriod) {} +} + +contract AuthorizableTest is Test, Bounder { + AuthorizableImp public authorizable; + AuthorizableHelper authHelper; + + modifier withFuzzyThaw(uint256 _thawPeriod) { + // Max thaw period is 1 year to allow for thawing tests + _thawPeriod = bound(_thawPeriod, 1, 60 * 60 * 24 * 365); + setupAuthorizable(new AuthorizableImp(_thawPeriod)); + _; + } + + function setUp() public virtual { + setupAuthorizable(new AuthorizableImp(0)); + } + + function setupAuthorizable(AuthorizableImp _authorizable) internal { + authorizable = _authorizable; + authHelper = new AuthorizableHelper(authorizable); + } + + function test_AuthorizeSigner(uint256 _unboundedKey, address _authorizer) public { + vm.assume(_authorizer != address(0)); + uint256 signerKey = boundKey(_unboundedKey); + + authHelper.authorizeSignerWithChecks(_authorizer, signerKey); + } + + function test_AuthorizeSigner_Revert_WhenAlreadyAuthorized( + uint256[] memory _unboundedAuthorizers, + uint256 _unboundedKey + ) public { + vm.assume(_unboundedAuthorizers.length > 1); + address[] memory authorizers = new address[](_unboundedAuthorizers.length); + for (uint256 i = 0; i < authorizers.length; i++) { + authorizers[i] = boundAddr(_unboundedAuthorizers[i]); + } + (uint256 signerKey, address signer) = boundAddrAndKey(_unboundedKey); + + address validAuthorizer = authorizers[0]; + authHelper.authorizeSignerWithChecks(validAuthorizer, signerKey); + + bytes memory expectedErr = abi.encodeWithSelector( + IAuthorizable.AuthorizableSignerAlreadyAuthorized.selector, + validAuthorizer, + signer, + false + ); + + for (uint256 i = 0; i < authorizers.length; i++) { + vm.expectRevert(expectedErr); + vm.prank(authorizers[i]); + authorizable.authorizeSigner(signer, 0, ""); + } + } + + function test_AuthorizeSigner_Revert_WhenInvalidProofDeadline(uint256 _proofDeadline, uint256 _now) public { + _proofDeadline = bound(_proofDeadline, 0, _now); + vm.warp(_now); + + bytes memory expectedErr = abi.encodeWithSelector( + IAuthorizable.AuthorizableInvalidSignerProofDeadline.selector, + _proofDeadline, + _now + ); + vm.expectRevert(expectedErr); + authorizable.authorizeSigner(address(0), _proofDeadline, ""); + } + + function test_AuthorizeSigner_Revert_WhenAuthorizableInvalidSignerProof( + uint256 _now, + uint256 _unboundedAuthorizer, + uint256 _unboundedKey, + uint256 _proofDeadline, + uint256 _chainid, + uint256 _wrong + ) public { + _now = bound(_now, 0, type(uint256).max - 1); + address authorizer = boundAddr(_unboundedAuthorizer); + (uint256 signerKey, address signer) = boundAddrAndKey(_unboundedKey); + _proofDeadline = boundTimestampMin(_proofDeadline, _now + 1); + vm.assume(_wrong != _proofDeadline); + _chainid = boundChainId(_chainid); + vm.assume(_wrong != _chainid); + (uint256 wrongKey, address wrongAddress) = boundAddrAndKey(_wrong); + vm.assume(wrongKey != signerKey); + vm.assume(wrongAddress != authorizer); + + vm.chainId(_chainid); + vm.warp(_now); + + bytes memory validProof = authHelper.generateAuthorizationProof( + _chainid, + address(authorizable), + _proofDeadline, + authorizer, + signerKey + ); + bytes[5] memory proofs = [ + authHelper.generateAuthorizationProof(_wrong, address(authorizable), _proofDeadline, authorizer, signerKey), + authHelper.generateAuthorizationProof(_chainid, wrongAddress, _proofDeadline, authorizer, signerKey), + authHelper.generateAuthorizationProof(_chainid, address(authorizable), _wrong, authorizer, signerKey), + authHelper.generateAuthorizationProof( + _chainid, + address(authorizable), + _proofDeadline, + wrongAddress, + signerKey + ), + authHelper.generateAuthorizationProof(_chainid, address(authorizable), _proofDeadline, authorizer, wrongKey) + ]; + + for (uint256 i = 0; i < proofs.length; i++) { + vm.expectRevert(IAuthorizable.AuthorizableInvalidSignerProof.selector); + vm.prank(authorizer); + authorizable.authorizeSigner(signer, _proofDeadline, proofs[i]); + } + + vm.prank(authorizer); + authorizable.authorizeSigner(signer, _proofDeadline, validProof); + authHelper.assertAuthorized(authorizer, signer); + } + + function test_ThawSigner(address _authorizer, uint256 _unboundedKey, uint256 _thaw) public withFuzzyThaw(_thaw) { + vm.assume(_authorizer != address(0)); + uint256 signerKey = boundKey(_unboundedKey); + + authHelper.authorizeAndThawSignerWithChecks(_authorizer, signerKey); + } + + function test_ThawSigner_Revert_WhenNotAuthorized(address _authorizer, address _signer) public { + vm.assume(_authorizer != address(0)); + vm.assume(_signer != address(0)); + + bytes memory expectedErr = abi.encodeWithSelector( + IAuthorizable.AuthorizableSignerNotAuthorized.selector, + _authorizer, + _signer + ); + vm.expectRevert(expectedErr); + vm.prank(_authorizer); + authorizable.thawSigner(_signer); + } + + function test_ThawSigner_Revert_WhenAuthorizationRevoked( + address _authorizer, + uint256 _unboundedKey, + uint256 _thaw + ) public withFuzzyThaw(_thaw) { + vm.assume(_authorizer != address(0)); + (uint256 signerKey, address signer) = boundAddrAndKey(_unboundedKey); + authHelper.authorizeAndRevokeSignerWithChecks(_authorizer, signerKey); + + bytes memory expectedErr = abi.encodeWithSelector( + IAuthorizable.AuthorizableSignerNotAuthorized.selector, + _authorizer, + signer + ); + vm.expectRevert(expectedErr); + vm.prank(_authorizer); + authorizable.thawSigner(signer); + } + + function test_CancelThawSigner( + address _authorizer, + uint256 _unboundedKey, + uint256 _thaw + ) public withFuzzyThaw(_thaw) { + vm.assume(_authorizer != address(0)); + (uint256 signerKey, address signer) = boundAddrAndKey(_unboundedKey); + + authHelper.authorizeAndThawSignerWithChecks(_authorizer, signerKey); + vm.expectEmit(address(authorizable)); + emit IAuthorizable.SignerThawCanceled(_authorizer, signer, authorizable.getThawEnd(signer)); + vm.prank(_authorizer); + authorizable.cancelThawSigner(signer); + + authHelper.assertAuthorized(_authorizer, signer); + } + + function test_CancelThawSigner_Revert_When_NotAuthorized(address _authorizer, address _signer) public { + vm.assume(_authorizer != address(0)); + vm.assume(_signer != address(0)); + + bytes memory expectedErr = abi.encodeWithSelector( + IAuthorizable.AuthorizableSignerNotAuthorized.selector, + _authorizer, + _signer + ); + vm.expectRevert(expectedErr); + vm.prank(_authorizer); + authorizable.cancelThawSigner(_signer); + } + + function test_CancelThawSigner_Revert_WhenAuthorizationRevoked( + address _authorizer, + uint256 _unboundedKey, + uint256 _thaw + ) public withFuzzyThaw(_thaw) { + vm.assume(_authorizer != address(0)); + (uint256 signerKey, address signer) = boundAddrAndKey(_unboundedKey); + authHelper.authorizeAndRevokeSignerWithChecks(_authorizer, signerKey); + + bytes memory expectedErr = abi.encodeWithSelector( + IAuthorizable.AuthorizableSignerNotAuthorized.selector, + _authorizer, + signer + ); + vm.expectRevert(expectedErr); + vm.prank(_authorizer); + authorizable.cancelThawSigner(signer); + } + + function test_CancelThawSigner_Revert_When_NotThawing(address _authorizer, uint256 _unboundedKey) public { + vm.assume(_authorizer != address(0)); + (uint256 signerKey, address signer) = boundAddrAndKey(_unboundedKey); + + authHelper.authorizeSignerWithChecks(_authorizer, signerKey); + + bytes memory expectedErr = abi.encodeWithSelector(IAuthorizable.AuthorizableSignerNotThawing.selector, signer); + vm.expectRevert(expectedErr); + vm.prank(_authorizer); + authorizable.cancelThawSigner(signer); + } + + function test_RevokeAuthorizedSigner( + address _authorizer, + uint256 _unboundedKey, + uint256 _thaw + ) public withFuzzyThaw(_thaw) { + vm.assume(_authorizer != address(0)); + uint256 signerKey = boundKey(_unboundedKey); + + authHelper.authorizeAndRevokeSignerWithChecks(_authorizer, signerKey); + } + + function test_RevokeAuthorizedSigner_Revert_WhenNotAuthorized(address _authorizer, address _signer) public { + vm.assume(_authorizer != address(0)); + vm.assume(_signer != address(0)); + + bytes memory expectedErr = abi.encodeWithSelector( + IAuthorizable.AuthorizableSignerNotAuthorized.selector, + _authorizer, + _signer + ); + vm.expectRevert(expectedErr); + vm.prank(_authorizer); + authorizable.revokeAuthorizedSigner(_signer); + } + + function test_RevokeAuthorizedSigner_Revert_WhenAuthorizationRevoked( + address _authorizer, + uint256 _unboundedKey, + uint256 _thaw + ) public withFuzzyThaw(_thaw) { + vm.assume(_authorizer != address(0)); + (uint256 signerKey, address signer) = boundAddrAndKey(_unboundedKey); + authHelper.authorizeAndRevokeSignerWithChecks(_authorizer, signerKey); + + bytes memory expectedErr = abi.encodeWithSelector( + IAuthorizable.AuthorizableSignerNotAuthorized.selector, + _authorizer, + signer + ); + vm.expectRevert(expectedErr); + vm.prank(_authorizer); + authorizable.revokeAuthorizedSigner(signer); + } + + function test_RevokeAuthorizedSigner_Revert_WhenNotThawing(address _authorizer, uint256 _unboundedKey) public { + vm.assume(_authorizer != address(0)); + (uint256 signerKey, address signer) = boundAddrAndKey(_unboundedKey); + + authHelper.authorizeSignerWithChecks(_authorizer, signerKey); + bytes memory expectedErr = abi.encodeWithSelector(IAuthorizable.AuthorizableSignerNotThawing.selector, signer); + vm.expectRevert(expectedErr); + vm.prank(_authorizer); + authorizable.revokeAuthorizedSigner(signer); + } + + function test_RevokeAuthorizedSigner_Revert_WhenStillThawing( + address _authorizer, + uint256 _unboundedKey, + uint256 _thaw, + uint256 _skip + ) public withFuzzyThaw(_thaw) { + vm.assume(_authorizer != address(0)); + (uint256 signerKey, address signer) = boundAddrAndKey(_unboundedKey); + + authHelper.authorizeAndThawSignerWithChecks(_authorizer, signerKey); + + _skip = bound(_skip, 0, authorizable.REVOKE_AUTHORIZATION_THAWING_PERIOD() - 1); + skip(_skip); + bytes memory expectedErr = abi.encodeWithSelector( + IAuthorizable.AuthorizableSignerStillThawing.selector, + block.timestamp, + block.timestamp - _skip + authorizable.REVOKE_AUTHORIZATION_THAWING_PERIOD() + ); + vm.expectRevert(expectedErr); + vm.prank(_authorizer); + authorizable.revokeAuthorizedSigner(signer); + } + + function test_IsAuthorized_Revert_WhenZero(address signer) public view { + authHelper.assertNotAuthorized(address(0), signer); + } +} + +contract AuthorizableHelper is Test { + AuthorizableImp internal authorizable; + + constructor(AuthorizableImp _authorizable) { + authorizable = _authorizable; + } + + function authorizeAndThawSignerWithChecks(address _authorizer, uint256 _signerKey) public { + address signer = vm.addr(_signerKey); + authorizeSignerWithChecks(_authorizer, _signerKey); + + uint256 thawEndTimestamp = block.timestamp + authorizable.REVOKE_AUTHORIZATION_THAWING_PERIOD(); + vm.expectEmit(address(authorizable)); + emit IAuthorizable.SignerThawing(_authorizer, signer, thawEndTimestamp); + vm.prank(_authorizer); + authorizable.thawSigner(signer); + + assertAuthorized(_authorizer, signer); + } + + function authorizeAndRevokeSignerWithChecks(address _authorizer, uint256 _signerKey) public { + address signer = vm.addr(_signerKey); + authorizeAndThawSignerWithChecks(_authorizer, _signerKey); + skip(authorizable.REVOKE_AUTHORIZATION_THAWING_PERIOD() + 1); + vm.expectEmit(address(authorizable)); + emit IAuthorizable.SignerRevoked(_authorizer, signer); + vm.prank(_authorizer); + authorizable.revokeAuthorizedSigner(signer); + + assertNotAuthorized(_authorizer, signer); + } + + function authorizeSignerWithChecks(address _authorizer, uint256 _signerKey) public { + address signer = vm.addr(_signerKey); + assertNotAuthorized(_authorizer, signer); + + uint256 proofDeadline = block.timestamp + 1; + bytes memory proof = generateAuthorizationProof( + block.chainid, + address(authorizable), + proofDeadline, + _authorizer, + _signerKey + ); + vm.expectEmit(address(authorizable)); + emit IAuthorizable.SignerAuthorized(_authorizer, signer); + vm.prank(_authorizer); + authorizable.authorizeSigner(signer, proofDeadline, proof); + + assertAuthorized(_authorizer, signer); + } + + function assertNotAuthorized(address _authorizer, address _signer) public view { + assertFalse(authorizable.isAuthorized(_authorizer, _signer), "Should not be authorized"); + } + + function assertAuthorized(address _authorizer, address _signer) public view { + assertTrue(authorizable.isAuthorized(_authorizer, _signer), "Should be authorized"); + } + + function generateAuthorizationProof( + uint256 _chainId, + address _verifyingContract, + uint256 _proofDeadline, + address _authorizer, + uint256 _signerPrivateKey + ) public pure returns (bytes memory) { + // Generate the message hash + bytes32 messageHash = keccak256( + abi.encodePacked(_chainId, _verifyingContract, "authorizeSignerProof", _proofDeadline, _authorizer) + ); + + // Generate the digest to sign + bytes32 digest = MessageHashUtils.toEthSignedMessageHash(messageHash); + + // Sign the digest + (uint8 v, bytes32 r, bytes32 s) = vm.sign(_signerPrivateKey, digest); + + // Encode the signature + return abi.encodePacked(r, s, v); + } +} diff --git a/packages/horizon/test/utils/Bounder.t.sol b/packages/horizon/test/utils/Bounder.t.sol new file mode 100644 index 000000000..44e977f57 --- /dev/null +++ b/packages/horizon/test/utils/Bounder.t.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.27; + +import { Test } from "forge-std/Test.sol"; + +contract Bounder is Test { + uint256 constant SECP256K1_CURVE_ORDER = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141; + + function boundAddrAndKey(uint256 _value) internal pure returns (uint256, address) { + uint256 signerKey = bound(_value, 1, SECP256K1_CURVE_ORDER - 1); + return (signerKey, vm.addr(signerKey)); + } + + function boundAddr(uint256 _value) internal pure returns (address) { + (, address addr) = boundAddrAndKey(_value); + return addr; + } + + function boundKey(uint256 _value) internal pure returns (uint256) { + (uint256 key, ) = boundAddrAndKey(_value); + return key; + } + + function boundChainId(uint256 _value) internal pure returns (uint256) { + return bound(_value, 1, (2 ^ 64) - 1); + } + + function boundTimestampMin(uint256 _value, uint256 _min) internal pure returns (uint256) { + return bound(_value, _min, type(uint256).max); + } +} diff --git a/packages/subgraph-service/test/subgraphService/SubgraphService.t.sol b/packages/subgraph-service/test/subgraphService/SubgraphService.t.sol index a3e3040bd..c83d43aac 100644 --- a/packages/subgraph-service/test/subgraphService/SubgraphService.t.sol +++ b/packages/subgraph-service/test/subgraphService/SubgraphService.t.sol @@ -281,7 +281,9 @@ contract SubgraphServiceTest is SubgraphServiceSharedTest { Allocation.State memory allocation = subgraphService.getAllocation( address(uint160(uint256(signedRav.rav.collectionId))) ); - (address payer, , ) = graphTallyCollector.authorizedSigners(_recoverRAVSigner(signedRav)); + address payer = graphTallyCollector.isAuthorized(signedRav.rav.payer, _recoverRAVSigner(signedRav)) + ? signedRav.rav.payer + : address(0); uint256 tokensCollected = graphTallyCollector.tokensCollected( address(subgraphService), diff --git a/packages/subgraph-service/test/subgraphService/collect/query/query.t.sol b/packages/subgraph-service/test/subgraphService/collect/query/query.t.sol index b16cccff5..28cc677cf 100644 --- a/packages/subgraph-service/test/subgraphService/collect/query/query.t.sol +++ b/packages/subgraph-service/test/subgraphService/collect/query/query.t.sol @@ -26,7 +26,13 @@ contract SubgraphServiceRegisterTest is SubgraphServiceTest { function _getSignerProof(uint256 _proofDeadline, uint256 _signer) private returns (bytes memory) { (, address msgSender, ) = vm.readCallers(); bytes32 messageHash = keccak256( - abi.encodePacked(block.chainid, address(graphTallyCollector), _proofDeadline, msgSender) + abi.encodePacked( + block.chainid, + address(graphTallyCollector), + "authorizeSignerProof", + _proofDeadline, + msgSender + ) ); bytes32 proofToDigest = MessageHashUtils.toEthSignedMessageHash(messageHash); (uint8 v, bytes32 r, bytes32 s) = vm.sign(_signer, proofToDigest);