Skip to content

Commit

Permalink
Add talent protocol discount validator (#97)
Browse files Browse the repository at this point in the history
* init

* First pass impl

* Natspec

* Add tests

* Add zero-address checks

* Address PR comments

* Fix typo

* Add clarity to possible revert case.
  • Loading branch information
stevieraykatz authored Oct 18, 2024
1 parent edbe86f commit 4e64f80
Show file tree
Hide file tree
Showing 5 changed files with 155 additions and 0 deletions.
71 changes: 71 additions & 0 deletions src/L2/discounts/TalentProtocolDiscountValidator.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
//SPDX-License-Identifier: MIT
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";

/// @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 {
/// @notice Thrown when setting a critical address to the zero-address.
error NoZeroAddress();

/// @notice Emitted when the threshold is updated by the `owner`.
event ThresholdUpdated(uint256 newThreshold);

/// @notice Interface with the Talent Protocol `PassportBuilderScore` contract.
TalentProtocol immutable talentProtocol;

/// @notice The score threshold for qualifying for a discount.
uint256 public threshold;

/// @notice constructor
///
/// @param owner_ The `Ownable` contract `owner`.
/// @param talentProtocol_ The address of the Talent Protocol PassportBuilderScore contract.
/// @param threshold_ The score required to qualify for a discount.
constructor(address owner_, address talentProtocol_, uint256 threshold_) {
if (owner_ == address(0)) revert NoZeroAddress();
if (talentProtocol_ == address(0)) revert NoZeroAddress();
talentProtocol = TalentProtocol(talentProtocol_);
threshold = threshold_;
_initializeOwner(owner_);
}

/// @notice Allows the `owner` to set the qualifying threshold.
///
/// @param threshold_ The new discount-qualifying threshold.
function setThreshold(uint256 threshold_) external onlyOwner {
threshold = threshold_;
emit ThresholdUpdated(threshold_);
}

/// @notice Required implementation for compatibility with IDiscountValidator.
///
/// @dev No additional data is required for validating this discount.
/// NOTE: The call to `getScoreByAddress` can revert. This case should be handled gracefully by integrators.
///
/// @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) {
return (talentProtocol.getScoreByAddress(claimer) >= threshold);
}
}

/// @notice Lightweight interface for the PassportBuilderScore.sol contract.
/// https://basescan.org/address/0xBBFeDA7c4d8d9Df752542b03CdD715F790B32D0B#readContract
interface TalentProtocol {
/// @notice Gets the score of a given address.
///
/// @param wallet The address to get the score for.
///
/// @return The score of the given address.
function getScoreByAddress(address wallet) external view returns (uint256);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;

import {TalentProtocolDiscountValidatorBase} from "./TalentProtocolDiscountValidatorBase.t.sol";

contract IsValidDiscountRegistration is TalentProtocolDiscountValidatorBase {
function test_returnsTrue_whenTheScoreMeetsTheThreshold() public {
talent.setScore(threshold);
bool ret = validator.isValidDiscountRegistration(userA, "");
assertTrue(ret);
}

function test_returnsTrue_whenTheScoreExceedsTheThreshold() public {
talent.setScore(threshold + 1);
bool ret = validator.isValidDiscountRegistration(userA, "");
assertTrue(ret);
}

function test_returnsFalse_whenTheScoreIsBelowTheThreshold() public {
talent.setScore(threshold - 1);
bool ret = validator.isValidDiscountRegistration(userA, "");
assertFalse(ret);
}
}
19 changes: 19 additions & 0 deletions test/discounts/TalenProtocolDiscountValidator/SetThreshold.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;

import {Ownable} from "solady/auth/Ownable.sol";
import {TalentProtocolDiscountValidatorBase} from "./TalentProtocolDiscountValidatorBase.t.sol";

contract SetThreshold is TalentProtocolDiscountValidatorBase {
function test_reverts_whenCalledByNonOwner() public {
vm.prank(userA);
vm.expectRevert(abi.encodeWithSelector(Ownable.Unauthorized.selector));
validator.setThreshold(0);
}

function test_allowsOwnerToSetThreshold() public {
vm.prank(owner);
validator.setThreshold(1);
assertEq(1, validator.threshold());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;

import {Test} from "forge-std/Test.sol";
import {TalentProtocolDiscountValidator} from "src/L2/discounts/TalentProtocolDiscountValidator.sol";
import {MockBuilderScorePassport} from "test/mocks/MockBuilderScorePassport.sol";

contract TalentProtocolDiscountValidatorBase is Test {
MockBuilderScorePassport talent;
TalentProtocolDiscountValidator validator;
address owner = makeAddr("owner");
address userA = makeAddr("userA");
address userB = makeAddr("userB");

uint256 threshold = 50;

function setUp() public {
talent = new MockBuilderScorePassport(threshold);
validator = new TalentProtocolDiscountValidator(owner, address(talent), threshold);
}
}
20 changes: 20 additions & 0 deletions test/mocks/MockBuilderScorePassport.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;

import {TalentProtocol} from "src/L2/discounts/TalentProtocolDiscountValidator.sol";

contract MockBuilderScorePassport is TalentProtocol {
uint256 score;

constructor(uint256 score_) {
score = score_;
}

function getScoreByAddress(address) external view returns (uint256) {
return score;
}

function setScore(uint256 score_) external {
score = score_;
}
}

0 comments on commit 4e64f80

Please sign in to comment.