Skip to content

Commit

Permalink
Updated ERC1155 Discount Validator (#100)
Browse files Browse the repository at this point in the history
* Added multi-id erc1155 discount validator

* lint

* test: add unit tests for ERC1155DiscountValidatorV2

* Address reentrancy vuln with balance check via staticcall

* lint

---------

Co-authored-by: Abdulla Al-Kamil <abdulla.alkamil@coinbase.com>
  • Loading branch information
stevieraykatz and abdulla-cb authored Nov 5, 2024
1 parent 4e64f80 commit 8f163b9
Show file tree
Hide file tree
Showing 3 changed files with 155 additions and 0 deletions.
66 changes: 66 additions & 0 deletions src/L2/discounts/ERC1155DiscountValidatorV2.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;

import {Address} from "@openzeppelin/contracts/utils/Address.sol";
import {IERC1155} from "@openzeppelin/contracts/token/ERC1155/IERC1155.sol";

import {IDiscountValidator} from "src/L2/interface/IDiscountValidator.sol";

/// @title Discount Validator for: ERC1155 NFTs
///
/// @notice Implements an NFT ownership validator for a stored mapping of `approvedTokenIds` for an ERC1155
/// `token` contract.
/// IMPORTANT: This discount validator should only be used for "soul-bound" tokens.
///
/// @author Coinbase (https://github.com/base-org/usernames)
contract ERC1155DiscountValidatorV2 is IDiscountValidator {
using Address for address;

/// @notice The ERC1155 token contract to validate against.
address immutable token;

/// @notice The approved token Ids of the ERC1155 token contract.
mapping(uint256 tokenId => bool approved) approvedTokenIds;

/// @notice ERC1155 Discount Validator constructor.
///
/// @param token_ The address of the token contract.
/// @param tokenIds The approved token ids the token `claimer` must hold.
constructor(address token_, uint256[] memory tokenIds) {
token = token_;
for (uint256 i; i < tokenIds.length; i++) {
approvedTokenIds[tokenIds[i]] = true;
}
}

/// @notice Required implementation for compatibility with IDiscountValidator.
///
/// @dev Encoded array of token Ids to check, set by `abi.encode(uint256[] ids)`
///
/// @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
view
override
returns (bool)
{
uint256[] memory ids = abi.decode(validationData, (uint256[]));
for (uint256 i; i < ids.length; i++) {
uint256 id = ids[i];
if (approvedTokenIds[id] && _getBalance(claimer, id) > 0) {
return true;
}
}
return false;
}

/// @notice Helper for staticcalling getBalance to avoid reentrancy vector.
function _getBalance(address claimer, uint256 id) internal view returns (uint256) {
bytes memory data = abi.encodeWithSelector(IERC1155.balanceOf.selector, claimer, id);
(bytes memory returnData) = token.functionStaticCall(data);
return (abi.decode(returnData, (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 {Test} from "forge-std/Test.sol";
import {ERC1155DiscountValidatorV2} from "src/L2/discounts/ERC1155DiscountValidatorV2.sol";
import {MockERC1155} from "test/mocks/MockERC1155.sol";

contract ERC1155DiscountValidatorV2Base is Test {
ERC1155DiscountValidatorV2 validator;
MockERC1155 token;
uint256 firstValidTokenId = 1;
uint256 secondValidTokenId = 2;
uint256 invalidTokenId = type(uint256).max;
address userA = makeAddr("userA");
address userB = makeAddr("userB");

function setUp() public {
token = new MockERC1155();
uint256[] memory validTokens = new uint256[](2);
validTokens[0] = firstValidTokenId;
validTokens[1] = secondValidTokenId;
validator = new ERC1155DiscountValidatorV2(address(token), validTokens);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;

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

contract IsValidDiscountRegistration is ERC1155DiscountValidatorV2Base {
function test_returnsTrue_whenTheUserHasOneToken() public {
uint256[] memory tokensToTest = new uint256[](1);
tokensToTest[0] = firstValidTokenId;
token.mint(userA, firstValidTokenId, 1);
assertTrue(validator.isValidDiscountRegistration(userA, abi.encode(tokensToTest)));
}

function test_returnsTrue_whenTheUserHasOneTokenProvidingMultipleIds() public {
uint256[] memory tokensToTest = new uint256[](2);
tokensToTest[0] = firstValidTokenId;
tokensToTest[1] = invalidTokenId;
token.mint(userA, firstValidTokenId, 1);
assertTrue(validator.isValidDiscountRegistration(userA, abi.encode(tokensToTest)));
}

function test_returnsTrue_whenTheUserHasMultipleTokens() public {
uint256[] memory tokensToTest = new uint256[](2);
tokensToTest[0] = firstValidTokenId;
tokensToTest[1] = secondValidTokenId;
token.mint(userA, firstValidTokenId, 1);
token.mint(userA, secondValidTokenId, 1);
assertTrue(validator.isValidDiscountRegistration(userA, abi.encode(tokensToTest)));
}

function test_returnsFalse_whenTheUserHasNoToken() public view {
uint256[] memory tokensToTest = new uint256[](1);
tokensToTest[0] = firstValidTokenId;
assertFalse(validator.isValidDiscountRegistration(userA, abi.encode(tokensToTest)));
}

function test_returnsFalse_whenAnotherUserHasAToken() public {
uint256[] memory tokensToTest = new uint256[](1);
tokensToTest[0] = firstValidTokenId;
token.mint(userB, firstValidTokenId, 1);
assertFalse(validator.isValidDiscountRegistration(userA, abi.encode(tokensToTest)));
}

function test_returnsFalse_whenTheUserHasAnInvalidToken() public {
uint256[] memory tokensToTest = new uint256[](3);
tokensToTest[0] = firstValidTokenId;
tokensToTest[1] = secondValidTokenId;
tokensToTest[2] = invalidTokenId;
token.mint(userA, invalidTokenId, 1);
assertFalse(validator.isValidDiscountRegistration(userA, abi.encode(tokensToTest)));
}

function test_returnsFalseWhenTheUserHasTokenButProvidesWrongList() public {
uint256[] memory tokensToTest = new uint256[](1);
tokensToTest[0] = secondValidTokenId;
token.mint(userA, firstValidTokenId, 1);
assertFalse(validator.isValidDiscountRegistration(userA, abi.encode(tokensToTest)));
}

function test_returnsFalseWhenUserProvidesEmptyList() public {
uint256[] memory tokensToTest = new uint256[](0);
token.mint(userA, firstValidTokenId, 1);
assertFalse(validator.isValidDiscountRegistration(userA, abi.encode(tokensToTest)));
}
}

0 comments on commit 8f163b9

Please sign in to comment.