Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

State-writeable discount validators #99

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 21 additions & 25 deletions src/L2/RegistrarController.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -232,25 +226,11 @@ contract RegistrarController is Ownable {
_;
}

/// @notice Decorator for validating discounted registrations.
///
/// @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.
/// @notice Decorator for validating a user for discounted registration.
///
/// @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);
}
_;
}

Expand Down Expand Up @@ -459,9 +439,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);
Expand Down Expand Up @@ -593,6 +575,20 @@ contract RegistrarController is Ownable {
active ? activeDiscounts.add(key) : activeDiscounts.remove(key);
}

/// @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);

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`.
function withdrawETH() public {
(bool sent,) = payable(paymentReceiver).call{value: (address(this).balance)}("");
Expand Down
6 changes: 3 additions & 3 deletions src/L2/discounts/AttestationValidator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;

Expand Down Expand Up @@ -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);
Expand Down
6 changes: 3 additions & 3 deletions src/L2/discounts/CBIdDiscountValidator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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)));
}
Expand Down
6 changes: 3 additions & 3 deletions src/L2/discounts/CouponDiscountValidator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@
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();

Expand Down Expand Up @@ -46,13 +46,13 @@
/// @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();

address returnedSigner = ECDSA.recover(_makeSignatureHash(claimer, uuid, expiry), sig);
return returnedSigner == signer;
}

Check notice

Code scanning / Slither

Block timestamp Low


/// @notice Generates a hash for signing/verifying.
///
Expand Down
41 changes: 41 additions & 0 deletions src/L2/discounts/DiscountValidator.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;

/// @title Discount Validato
///
/// @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 address of the claiming user.
/// @param data The associated `validationData`.
error InvalidDiscount(address claimer, bytes data);

/// @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`.
/// 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) public virtual view returns (bool);


/// @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);
}
}
6 changes: 3 additions & 3 deletions src/L2/discounts/ERC1155DiscountValidator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ 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
///
/// @notice Implements an NFT ownership validator for a stored `tokenId` for an ERC1155 `token` contract.
/// 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;

Expand All @@ -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);
}
}
6 changes: 3 additions & 3 deletions src/L2/discounts/ERC721DiscountValidator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ 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
///
/// @notice Implements an NFT ownership validator for a ERC721 `token` contract.
/// 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;

Expand All @@ -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);
}
}
6 changes: 3 additions & 3 deletions src/L2/discounts/TalentProtocolDiscountValidator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ 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
///
/// @notice Enables discounts for users who have minted their Talent Protocol Builder Score .
/// 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();

Expand Down Expand Up @@ -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);
}
}
Expand Down
8 changes: 6 additions & 2 deletions test/RegistrarController/DiscountedRegister.t.sol
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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);
Expand All @@ -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, "");
}
Expand Down Expand Up @@ -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, "");
}
Expand Down
6 changes: 3 additions & 3 deletions test/mocks/MockDiscountValidator.sol
Original file line number Diff line number Diff line change
@@ -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;
}

Expand Down
Loading