From c548aefaedbdd8ed8d82338dcb58b4881ca383fb Mon Sep 17 00:00:00 2001 From: katzman Date: Tue, 29 Oct 2024 16:50:32 -0700 Subject: [PATCH 1/2] Refactor Discount Validator to be an abstract contract supporting state writes in validation --- src/L2/RegistrarController.sol | 36 +++++++++---------- src/L2/discounts/AttestationValidator.sol | 6 ++-- src/L2/discounts/CBIdDiscountValidator.sol | 6 ++-- src/L2/discounts/CouponDiscountValidator.sol | 6 ++-- .../DiscountValidator.sol} | 17 +++++++-- src/L2/discounts/ERC1155DiscountValidator.sol | 6 ++-- src/L2/discounts/ERC721DiscountValidator.sol | 6 ++-- .../TalentProtocolDiscountValidator.sol | 6 ++-- 8 files changed, 50 insertions(+), 39 deletions(-) rename src/L2/{interface/IDiscountValidator.sol => discounts/DiscountValidator.sol} (55%) diff --git a/src/L2/RegistrarController.sol b/src/L2/RegistrarController.sol index 4c35c9b5..93740f8f 100644 --- a/src/L2/RegistrarController.sol +++ b/src/L2/RegistrarController.sol @@ -232,25 +232,11 @@ contract RegistrarController is Ownable { _; } - /// @notice Decorator for validating discounted registrations. + /// @notice Decorator for validating a user for discounted registration. /// - /// @dev Validates that: - /// 1. That the registrant has not already registered with a discount - /// 2. That the discount is `active` - /// 3. That the associated `discountValidator` returns true when `isValidDiscountRegistration` is called. - /// - /// @param discountKey The uuid of the discount. - /// @param validationData The associated validation data for this discount registration. - modifier validDiscount(bytes32 discountKey, bytes calldata validationData) { + /// @dev Validates that that the registrant has not already registered with a discount + modifier discountAvailable() { if (discountedRegistrants[msg.sender]) revert AlreadyRegisteredWithDiscount(msg.sender); - DiscountDetails memory details = discounts[discountKey]; - - if (!details.active) revert InactiveDiscount(discountKey); - - IDiscountValidator validator = IDiscountValidator(details.discountValidator); - if (!validator.isValidDiscountRegistration(msg.sender, validationData)) { - revert InvalidDiscount(discountKey, validationData); - } _; } @@ -459,9 +445,11 @@ contract RegistrarController is Ownable { function discountedRegister(RegisterRequest calldata request, bytes32 discountKey, bytes calldata validationData) public payable - validDiscount(discountKey, validationData) validRegistration(request) + discountAvailable { + _validateDiscount(discountKey, validationData); + uint256 price = discountedRegisterPrice(request.name, request.duration, discountKey); _validatePayment(price); @@ -593,6 +581,18 @@ contract RegistrarController is Ownable { active ? activeDiscounts.add(key) : activeDiscounts.remove(key); } + /// Validates that: + /// 1. That the discount is `active` + /// 2. That the associated `discountValidator` returns true when `isValidDiscountRegistration` is called. + function _validateDiscount(bytes32 discountKey, bytes calldata validationData) internal { + DiscountDetails memory details = discounts[discountKey]; + + IDiscountValidator validator = IDiscountValidator(details.discountValidator); + // if (!validator.validateDiscountRegistration(msg.sender, validationData)) { + // revert InvalidDiscount(discountKey, validationData); + // } + } + /// @notice Allows anyone to withdraw the eth accumulated on this contract back to the `paymentReceiver`. function withdrawETH() public { (bool sent,) = payable(paymentReceiver).call{value: (address(this).balance)}(""); diff --git a/src/L2/discounts/AttestationValidator.sol b/src/L2/discounts/AttestationValidator.sol index b841c9b5..8c66c67b 100644 --- a/src/L2/discounts/AttestationValidator.sol +++ b/src/L2/discounts/AttestationValidator.sol @@ -6,7 +6,7 @@ import {AttestationVerifier} from "verifications/libraries/AttestationVerifier.s import {IAttestationIndexer} from "verifications/interfaces/IAttestationIndexer.sol"; import {Ownable} from "solady/auth/Ownable.sol"; -import {IDiscountValidator} from "src/L2/interface/IDiscountValidator.sol"; +import {DiscountValidator} from "./DiscountValidator.sol"; import {SybilResistanceVerifier} from "src/lib/SybilResistanceVerifier.sol"; /// @title Discount Validator for: Coinbase Attestation Validator @@ -17,7 +17,7 @@ import {SybilResistanceVerifier} from "src/lib/SybilResistanceVerifier.sol"; /// https://github.com/coinbase/verifications /// /// @author Coinbase (https://github.com/base-org/usernames) -contract AttestationValidator is Ownable, AttestationAccessControl, IDiscountValidator { +contract AttestationValidator is Ownable, AttestationAccessControl, DiscountValidator { /// @dev The attestation service signer. address signer; @@ -52,7 +52,7 @@ contract AttestationValidator is Ownable, AttestationAccessControl, IDiscountVal /// @param validationData opaque bytes for performing the validation. /// /// @return `true` if the validation data provided is determined to be valid for the specified claimer, else `false`. - function isValidDiscountRegistration(address claimer, bytes calldata validationData) external view returns (bool) { + function isValidDiscountRegistration(address claimer, bytes calldata validationData) public override view returns (bool) { AttestationVerifier.verifyAttestation(_getAttestation(claimer, schemaID)); return SybilResistanceVerifier.verifySignature(signer, claimer, validationData); diff --git a/src/L2/discounts/CBIdDiscountValidator.sol b/src/L2/discounts/CBIdDiscountValidator.sol index f5a8a160..3c5b14b3 100644 --- a/src/L2/discounts/CBIdDiscountValidator.sol +++ b/src/L2/discounts/CBIdDiscountValidator.sol @@ -4,14 +4,14 @@ pragma solidity ^0.8.23; import {MerkleProofLib} from "lib/solady/src/utils/MerkleProofLib.sol"; import {Ownable} from "solady/auth/Ownable.sol"; -import {IDiscountValidator} from "src/L2/interface/IDiscountValidator.sol"; +import {DiscountValidator} from "./DiscountValidator.sol"; /// @title Discount Validator for: cb.id /// /// @notice Implements a simple Merkle Proof validator checking that the claimant is in the stored merkle tree. /// /// @author Coinbase -contract CBIdDiscountValidator is Ownable, IDiscountValidator { +contract CBIdDiscountValidator is Ownable, DiscountValidator { /// @dev merkle tree root bytes32 public root; @@ -35,7 +35,7 @@ contract CBIdDiscountValidator is Ownable, IDiscountValidator { /// @param validationData opaque bytes for performing the validation. /// /// @return `true` if the validation data provided is determined to be valid for the specified claimer, else `false`. - function isValidDiscountRegistration(address claimer, bytes calldata validationData) external view returns (bool) { + function isValidDiscountRegistration(address claimer, bytes calldata validationData) public view override returns (bool) { (bytes32[] memory proof) = abi.decode(validationData, (bytes32[])); return MerkleProofLib.verify(proof, root, keccak256(abi.encodePacked(claimer))); } diff --git a/src/L2/discounts/CouponDiscountValidator.sol b/src/L2/discounts/CouponDiscountValidator.sol index 85a96338..24c1f0f7 100644 --- a/src/L2/discounts/CouponDiscountValidator.sol +++ b/src/L2/discounts/CouponDiscountValidator.sol @@ -4,14 +4,14 @@ pragma solidity ^0.8.23; import {ECDSA} from "solady/utils/ECDSA.sol"; import {Ownable} from "solady/auth/Ownable.sol"; -import {IDiscountValidator} from "src/L2/interface/IDiscountValidator.sol"; +import {DiscountValidator} from "./DiscountValidator.sol"; /// @title Discount Validator for: Coupons /// /// @notice Implements a signature-based discount validation on unique coupon codes. /// /// @author Coinbase (https://github.com/base-org/usernames) -contract CouponDiscountValidator is Ownable, IDiscountValidator { +contract CouponDiscountValidator is Ownable, DiscountValidator { /// @notice Thrown when setting a critical address to the zero-address. error NoZeroAddress(); @@ -46,7 +46,7 @@ contract CouponDiscountValidator is Ownable, IDiscountValidator { /// @param validationData opaque bytes for performing the validation. /// /// @return `true` if the validation data provided is determined to be valid for the specified claimer, else `false`. - function isValidDiscountRegistration(address claimer, bytes calldata validationData) external view returns (bool) { + function isValidDiscountRegistration(address claimer, bytes calldata validationData) public view override returns (bool) { (uint64 expiry, bytes32 uuid, bytes memory sig) = abi.decode(validationData, (uint64, bytes32, bytes)); if (expiry < block.timestamp) revert SignatureExpired(); diff --git a/src/L2/interface/IDiscountValidator.sol b/src/L2/discounts/DiscountValidator.sol similarity index 55% rename from src/L2/interface/IDiscountValidator.sol rename to src/L2/discounts/DiscountValidator.sol index 100ad959..82325566 100644 --- a/src/L2/interface/IDiscountValidator.sol +++ b/src/L2/discounts/DiscountValidator.sol @@ -5,8 +5,15 @@ pragma solidity ^0.8.23; /// /// @notice Common interface which all Discount Validators must implement. /// The logic specific to each integration must ultimately be consumable as the `bool` returned from -/// `isValidDiscountRegistration`. -interface IDiscountValidator { +/// `isValidDiscountRegistration`. Then, upon registration, the integrator should call `validateDiscountRegistration` +/// allowing discount-specific state writes to occur. +abstract contract DiscountValidator { + /// @notice Thrown when the specified discount's validator does not accept the discount for the sender. + /// + /// @param claimer The discount being accessed. + /// @param data The associated `validationData`. + error InvalidDiscount(address claimer, bytes data); + /// @notice Required implementation for compatibility with IDiscountValidator. /// /// @dev Each implementation will have unique requirements for the data necessary to perform @@ -17,5 +24,9 @@ interface IDiscountValidator { /// @param validationData opaque bytes for performing the validation. /// /// @return `true` if the validation data provided is determined to be valid for the specified claimer, else `false`. - function isValidDiscountRegistration(address claimer, bytes calldata validationData) external returns (bool); + function isValidDiscountRegistration(address claimer, bytes calldata validationData) public virtual view returns (bool); + + function validateDiscountRegistration(address claimer, bytes calldata validationData) external virtual view { + if(!isValidDiscountRegistration(claimer, validationData)) revert InvalidDiscount(claimer, validationData); + } } diff --git a/src/L2/discounts/ERC1155DiscountValidator.sol b/src/L2/discounts/ERC1155DiscountValidator.sol index 5deeb122..a097be7e 100644 --- a/src/L2/discounts/ERC1155DiscountValidator.sol +++ b/src/L2/discounts/ERC1155DiscountValidator.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.23; import {IERC1155} from "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; -import {IDiscountValidator} from "src/L2/interface/IDiscountValidator.sol"; +import {DiscountValidator} from "./DiscountValidator.sol"; /// @title Discount Validator for: ERC1155 NFTs /// @@ -11,7 +11,7 @@ import {IDiscountValidator} from "src/L2/interface/IDiscountValidator.sol"; /// This discount validator should only be used for "soul-bound" tokens. /// /// @author Coinbase (https://github.com/base-org/usernames) -contract ERC1155DiscountValidator is IDiscountValidator { +contract ERC1155DiscountValidator is DiscountValidator { /// @notice The ERC1155 token contract to validate against. IERC1155 immutable token; @@ -35,7 +35,7 @@ contract ERC1155DiscountValidator is IDiscountValidator { /// @param claimer the discount claimer's address. /// /// @return `true` if the validation data provided is determined to be valid for the specified claimer, else `false`. - function isValidDiscountRegistration(address claimer, bytes calldata) external view returns (bool) { + function isValidDiscountRegistration(address claimer, bytes calldata) public view override returns (bool) { return (token.balanceOf(claimer, tokenId) > 0); } } diff --git a/src/L2/discounts/ERC721DiscountValidator.sol b/src/L2/discounts/ERC721DiscountValidator.sol index baea7c87..58179cf3 100644 --- a/src/L2/discounts/ERC721DiscountValidator.sol +++ b/src/L2/discounts/ERC721DiscountValidator.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.23; import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; -import {IDiscountValidator} from "src/L2/interface/IDiscountValidator.sol"; +import {DiscountValidator} from "./DiscountValidator.sol"; /// @title Discount Validator for: ERC721 NFTs /// @@ -11,7 +11,7 @@ import {IDiscountValidator} from "src/L2/interface/IDiscountValidator.sol"; /// This discount validator should only be used for "soul-bound" tokens. /// /// @author Coinbase (https://github.com/base-org/usernames) -contract ERC721DiscountValidator is IDiscountValidator { +contract ERC721DiscountValidator is DiscountValidator { /// @notice The ERC721 token contract to validate against. IERC721 immutable token; @@ -30,7 +30,7 @@ contract ERC721DiscountValidator is IDiscountValidator { /// @param claimer the discount claimer's address. /// /// @return `true` if the validation data provided is determined to be valid for the specified claimer, else `false`. - function isValidDiscountRegistration(address claimer, bytes calldata) external view returns (bool) { + function isValidDiscountRegistration(address claimer, bytes calldata) public view override returns (bool) { return (token.balanceOf(claimer) > 0); } } diff --git a/src/L2/discounts/TalentProtocolDiscountValidator.sol b/src/L2/discounts/TalentProtocolDiscountValidator.sol index 06679891..8d405b6c 100644 --- a/src/L2/discounts/TalentProtocolDiscountValidator.sol +++ b/src/L2/discounts/TalentProtocolDiscountValidator.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.23; import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; import {Ownable} from "solady/auth/Ownable.sol"; -import {IDiscountValidator} from "src/L2/interface/IDiscountValidator.sol"; +import {DiscountValidator} from "./DiscountValidator.sol"; /// @title Discount Validator for: Talent Protocol Builder Score /// @@ -12,7 +12,7 @@ import {IDiscountValidator} from "src/L2/interface/IDiscountValidator.sol"; /// Discounts are granted based on the claimer having some score higher than this contract's `threshold`. /// /// @author Coinbase (https://github.com/base-org/usernames) -contract TalentProtocolDiscountValidator is IDiscountValidator, Ownable { +contract TalentProtocolDiscountValidator is DiscountValidator, Ownable { /// @notice Thrown when setting a critical address to the zero-address. error NoZeroAddress(); @@ -54,7 +54,7 @@ contract TalentProtocolDiscountValidator is IDiscountValidator, Ownable { /// @param claimer the discount claimer's address. /// /// @return `true` if the validation data provided is determined to be valid for the specified claimer, else `false`. - function isValidDiscountRegistration(address claimer, bytes calldata) external view returns (bool) { + function isValidDiscountRegistration(address claimer, bytes calldata) public view override returns (bool) { return (talentProtocol.getScoreByAddress(claimer) >= threshold); } } From ca4e6d4cdc15ba0536e74871b4c6bc210e73f38a Mon Sep 17 00:00:00 2001 From: katzman Date: Wed, 30 Oct 2024 12:20:40 -0700 Subject: [PATCH 2/2] Fixes and tests --- src/L2/RegistrarController.sol | 24 ++++++++---------- src/L2/discounts/DiscountValidator.sol | 25 +++++++++++++------ src/L2/interface/IDiscountValidator.sol | 21 ++++++++++++++++ .../DiscountedRegister.t.sol | 8 ++++-- test/mocks/MockDiscountValidator.sol | 6 ++--- 5 files changed, 57 insertions(+), 27 deletions(-) create mode 100644 src/L2/interface/IDiscountValidator.sol diff --git a/src/L2/RegistrarController.sol b/src/L2/RegistrarController.sol index 93740f8f..ab8a3ce0 100644 --- a/src/L2/RegistrarController.sol +++ b/src/L2/RegistrarController.sol @@ -9,7 +9,7 @@ import {StringUtils} from "ens-contracts/ethregistrar/StringUtils.sol"; import {BASE_ETH_NODE, GRACE_PERIOD} from "src/util/Constants.sol"; import {BaseRegistrar} from "./BaseRegistrar.sol"; -import {IDiscountValidator} from "./interface/IDiscountValidator.sol"; +import {DiscountValidator} from "./discounts/DiscountValidator.sol"; import {IPriceOracle} from "./interface/IPriceOracle.sol"; import {L2Resolver} from "./L2Resolver.sol"; import {IReverseRegistrar} from "./interface/IReverseRegistrar.sol"; @@ -132,12 +132,6 @@ contract RegistrarController is Ownable { /// @notice Thrown when the payment received is less than the price. error InsufficientValue(); - /// @notice Thrown when the specified discount's validator does not accept the discount for the sender. - /// - /// @param key The discount being accessed. - /// @param data The associated `validationData`. - error InvalidDiscount(bytes32 key, bytes data); - /// @notice Thrown when the discount amount is 0. /// /// @param key The discount being set. @@ -581,16 +575,18 @@ contract RegistrarController is Ownable { active ? activeDiscounts.add(key) : activeDiscounts.remove(key); } - /// Validates that: - /// 1. That the discount is `active` - /// 2. That the associated `discountValidator` returns true when `isValidDiscountRegistration` is called. + /// @notice Calls the associated discount validator with `msg.sender` and `validationData`. + /// + /// @dev This method calls `validateDiscountRegistration` which may revert with `DiscountValidator.InvalidDiscount`. + /// + /// @param discountKey unique identifier for the discount. + /// @param validationData validation data required for discount. function _validateDiscount(bytes32 discountKey, bytes calldata validationData) internal { DiscountDetails memory details = discounts[discountKey]; + if(!details.active) revert InactiveDiscount(discountKey); - IDiscountValidator validator = IDiscountValidator(details.discountValidator); - // if (!validator.validateDiscountRegistration(msg.sender, validationData)) { - // revert InvalidDiscount(discountKey, validationData); - // } + DiscountValidator validator = DiscountValidator(details.discountValidator); + validator.validateDiscountRegistration(msg.sender, validationData); } /// @notice Allows anyone to withdraw the eth accumulated on this contract back to the `paymentReceiver`. diff --git a/src/L2/discounts/DiscountValidator.sol b/src/L2/discounts/DiscountValidator.sol index 82325566..1dd4d6bb 100644 --- a/src/L2/discounts/DiscountValidator.sol +++ b/src/L2/discounts/DiscountValidator.sol @@ -1,20 +1,20 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.23; -/// @title Discount Validator Interface +/// @title Discount Validato /// -/// @notice Common interface which all Discount Validators must implement. -/// The logic specific to each integration must ultimately be consumable as the `bool` returned from -/// `isValidDiscountRegistration`. Then, upon registration, the integrator should call `validateDiscountRegistration` -/// allowing discount-specific state writes to occur. +/// @notice Discount Validator base contract which must be inherited by implementing validators. +/// The logic specific to each integration must ultimately be consumable as: +/// 1. A `bool` returned from `isValidDiscountRegistration` for offchain pre-tx validation, and +/// 2. A call to `validateDiscountRegistration` which will revert if validation fails abstract contract DiscountValidator { /// @notice Thrown when the specified discount's validator does not accept the discount for the sender. /// - /// @param claimer The discount being accessed. + /// @param claimer The address of the claiming user. /// @param data The associated `validationData`. error InvalidDiscount(address claimer, bytes data); - /// @notice Required implementation for compatibility with IDiscountValidator. + /// @notice Required implementation for compatibility with DiscountValidator. /// /// @dev Each implementation will have unique requirements for the data necessary to perform /// a meaningul validation. Implementations must describe here how to pack relevant `validationData`. @@ -26,7 +26,16 @@ abstract contract DiscountValidator { /// @return `true` if the validation data provided is determined to be valid for the specified claimer, else `false`. function isValidDiscountRegistration(address claimer, bytes calldata validationData) public virtual view returns (bool); - function validateDiscountRegistration(address claimer, bytes calldata validationData) external virtual view { + + /// @notice Required implementation for compaibility with DiscountValidator. + /// + /// @dev This method reverts with `InvalidDiscount` if called with for an invalid combination of `claimer` and `validationData`. + /// By default, it simply calls `isValidDiscountRegistration`. If more sophisticated state tracking is required, overwrite this + /// method. Overwriten methods MUST still revert with `InvalidDiscount` should the data fail the validation step. + /// + /// @param claimer the discount claimer's address. + /// @param validationData opaque bytes for performing the validation. + function validateDiscountRegistration(address claimer, bytes calldata validationData) external virtual { if(!isValidDiscountRegistration(claimer, validationData)) revert InvalidDiscount(claimer, validationData); } } diff --git a/src/L2/interface/IDiscountValidator.sol b/src/L2/interface/IDiscountValidator.sol new file mode 100644 index 00000000..100ad959 --- /dev/null +++ b/src/L2/interface/IDiscountValidator.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +/// @title Discount Validator Interface +/// +/// @notice Common interface which all Discount Validators must implement. +/// The logic specific to each integration must ultimately be consumable as the `bool` returned from +/// `isValidDiscountRegistration`. +interface IDiscountValidator { + /// @notice Required implementation for compatibility with IDiscountValidator. + /// + /// @dev Each implementation will have unique requirements for the data necessary to perform + /// a meaningul validation. Implementations must describe here how to pack relevant `validationData`. + /// Ex: `bytes validationData = abi.encode(bytes32 key, bytes32[] proof)` + /// + /// @param claimer the discount claimer's address. + /// @param validationData opaque bytes for performing the validation. + /// + /// @return `true` if the validation data provided is determined to be valid for the specified claimer, else `false`. + function isValidDiscountRegistration(address claimer, bytes calldata validationData) external returns (bool); +} diff --git a/test/RegistrarController/DiscountedRegister.t.sol b/test/RegistrarController/DiscountedRegister.t.sol index b4498d6e..031330e2 100644 --- a/test/RegistrarController/DiscountedRegister.t.sol +++ b/test/RegistrarController/DiscountedRegister.t.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.23; +import {DiscountValidator} from "src/L2/discounts/DiscountValidator.sol"; import {RegistrarControllerBase} from "./RegistrarControllerBase.t.sol"; import {RegistrarController} from "src/L2/RegistrarController.sol"; import {IPriceOracle} from "src/L2/interface/IPriceOracle.sol"; @@ -11,6 +12,7 @@ contract DiscountedRegister is RegistrarControllerBase { vm.deal(user, 1 ether); inactiveDiscount.active = false; + base.setAvailable(uint256(nameLabel), true); vm.prank(owner); controller.setDiscountDetails(inactiveDiscount); uint256 price = controller.discountedRegisterPrice(name, duration, discountKey); @@ -26,8 +28,9 @@ contract DiscountedRegister is RegistrarControllerBase { controller.setDiscountDetails(_getDefaultDiscount()); validator.setReturnValue(false); uint256 price = controller.discountedRegisterPrice(name, duration, discountKey); + base.setAvailable(uint256(nameLabel), true); - vm.expectRevert(abi.encodeWithSelector(RegistrarController.InvalidDiscount.selector, discountKey, "")); + vm.expectRevert(abi.encodeWithSelector(DiscountValidator.InvalidDiscount.selector, user, "")); vm.prank(user); controller.discountedRegister{value: price}(_getDefaultRegisterRequest(), discountKey, ""); } @@ -136,8 +139,9 @@ contract DiscountedRegister is RegistrarControllerBase { vm.prank(user); controller.discountedRegister{value: price}(request, discountKey, ""); - vm.expectRevert(abi.encodeWithSelector(RegistrarController.AlreadyRegisteredWithDiscount.selector, user)); request.name = "newname"; + base.setAvailable(uint256(keccak256(bytes(request.name))),true); + vm.expectRevert(abi.encodeWithSelector(RegistrarController.AlreadyRegisteredWithDiscount.selector, user)); vm.prank(user); controller.discountedRegister{value: price}(request, discountKey, ""); } diff --git a/test/mocks/MockDiscountValidator.sol b/test/mocks/MockDiscountValidator.sol index be7de9a1..1d7432b7 100644 --- a/test/mocks/MockDiscountValidator.sol +++ b/test/mocks/MockDiscountValidator.sol @@ -1,12 +1,12 @@ //SPDX-License-Identifier: MIT pragma solidity ^0.8.23; -import "src/L2/interface/IDiscountValidator.sol"; +import "src/L2/discounts/DiscountValidator.sol"; -contract MockDiscountValidator is IDiscountValidator { +contract MockDiscountValidator is DiscountValidator { bool returnValue = true; - function isValidDiscountRegistration(address, bytes calldata) external view returns (bool) { + function isValidDiscountRegistration(address, bytes calldata) public view override returns (bool) { return returnValue; }