Skip to content

Commit 8f163b9

Browse files
Updated ERC1155 Discount Validator (#100)
* 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 <[email protected]>
1 parent 4e64f80 commit 8f163b9

File tree

3 files changed

+155
-0
lines changed

3 files changed

+155
-0
lines changed
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
//SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.23;
3+
4+
import {Address} from "@openzeppelin/contracts/utils/Address.sol";
5+
import {IERC1155} from "@openzeppelin/contracts/token/ERC1155/IERC1155.sol";
6+
7+
import {IDiscountValidator} from "src/L2/interface/IDiscountValidator.sol";
8+
9+
/// @title Discount Validator for: ERC1155 NFTs
10+
///
11+
/// @notice Implements an NFT ownership validator for a stored mapping of `approvedTokenIds` for an ERC1155
12+
/// `token` contract.
13+
/// IMPORTANT: This discount validator should only be used for "soul-bound" tokens.
14+
///
15+
/// @author Coinbase (https://github.com/base-org/usernames)
16+
contract ERC1155DiscountValidatorV2 is IDiscountValidator {
17+
using Address for address;
18+
19+
/// @notice The ERC1155 token contract to validate against.
20+
address immutable token;
21+
22+
/// @notice The approved token Ids of the ERC1155 token contract.
23+
mapping(uint256 tokenId => bool approved) approvedTokenIds;
24+
25+
/// @notice ERC1155 Discount Validator constructor.
26+
///
27+
/// @param token_ The address of the token contract.
28+
/// @param tokenIds The approved token ids the token `claimer` must hold.
29+
constructor(address token_, uint256[] memory tokenIds) {
30+
token = token_;
31+
for (uint256 i; i < tokenIds.length; i++) {
32+
approvedTokenIds[tokenIds[i]] = true;
33+
}
34+
}
35+
36+
/// @notice Required implementation for compatibility with IDiscountValidator.
37+
///
38+
/// @dev Encoded array of token Ids to check, set by `abi.encode(uint256[] ids)`
39+
///
40+
/// @param claimer the discount claimer's address.
41+
/// @param validationData opaque bytes for performing the validation.
42+
///
43+
/// @return `true` if the validation data provided is determined to be valid for the specified claimer, else `false`.
44+
function isValidDiscountRegistration(address claimer, bytes calldata validationData)
45+
public
46+
view
47+
override
48+
returns (bool)
49+
{
50+
uint256[] memory ids = abi.decode(validationData, (uint256[]));
51+
for (uint256 i; i < ids.length; i++) {
52+
uint256 id = ids[i];
53+
if (approvedTokenIds[id] && _getBalance(claimer, id) > 0) {
54+
return true;
55+
}
56+
}
57+
return false;
58+
}
59+
60+
/// @notice Helper for staticcalling getBalance to avoid reentrancy vector.
61+
function _getBalance(address claimer, uint256 id) internal view returns (uint256) {
62+
bytes memory data = abi.encodeWithSelector(IERC1155.balanceOf.selector, claimer, id);
63+
(bytes memory returnData) = token.functionStaticCall(data);
64+
return (abi.decode(returnData, (uint256)));
65+
}
66+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
//SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.23;
3+
4+
import {Test} from "forge-std/Test.sol";
5+
import {ERC1155DiscountValidatorV2} from "src/L2/discounts/ERC1155DiscountValidatorV2.sol";
6+
import {MockERC1155} from "test/mocks/MockERC1155.sol";
7+
8+
contract ERC1155DiscountValidatorV2Base is Test {
9+
ERC1155DiscountValidatorV2 validator;
10+
MockERC1155 token;
11+
uint256 firstValidTokenId = 1;
12+
uint256 secondValidTokenId = 2;
13+
uint256 invalidTokenId = type(uint256).max;
14+
address userA = makeAddr("userA");
15+
address userB = makeAddr("userB");
16+
17+
function setUp() public {
18+
token = new MockERC1155();
19+
uint256[] memory validTokens = new uint256[](2);
20+
validTokens[0] = firstValidTokenId;
21+
validTokens[1] = secondValidTokenId;
22+
validator = new ERC1155DiscountValidatorV2(address(token), validTokens);
23+
}
24+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
//SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.23;
3+
4+
import {ERC1155DiscountValidatorV2Base} from "./ERC1155DiscountValidatorV2Base.t.sol";
5+
6+
contract IsValidDiscountRegistration is ERC1155DiscountValidatorV2Base {
7+
function test_returnsTrue_whenTheUserHasOneToken() public {
8+
uint256[] memory tokensToTest = new uint256[](1);
9+
tokensToTest[0] = firstValidTokenId;
10+
token.mint(userA, firstValidTokenId, 1);
11+
assertTrue(validator.isValidDiscountRegistration(userA, abi.encode(tokensToTest)));
12+
}
13+
14+
function test_returnsTrue_whenTheUserHasOneTokenProvidingMultipleIds() public {
15+
uint256[] memory tokensToTest = new uint256[](2);
16+
tokensToTest[0] = firstValidTokenId;
17+
tokensToTest[1] = invalidTokenId;
18+
token.mint(userA, firstValidTokenId, 1);
19+
assertTrue(validator.isValidDiscountRegistration(userA, abi.encode(tokensToTest)));
20+
}
21+
22+
function test_returnsTrue_whenTheUserHasMultipleTokens() public {
23+
uint256[] memory tokensToTest = new uint256[](2);
24+
tokensToTest[0] = firstValidTokenId;
25+
tokensToTest[1] = secondValidTokenId;
26+
token.mint(userA, firstValidTokenId, 1);
27+
token.mint(userA, secondValidTokenId, 1);
28+
assertTrue(validator.isValidDiscountRegistration(userA, abi.encode(tokensToTest)));
29+
}
30+
31+
function test_returnsFalse_whenTheUserHasNoToken() public view {
32+
uint256[] memory tokensToTest = new uint256[](1);
33+
tokensToTest[0] = firstValidTokenId;
34+
assertFalse(validator.isValidDiscountRegistration(userA, abi.encode(tokensToTest)));
35+
}
36+
37+
function test_returnsFalse_whenAnotherUserHasAToken() public {
38+
uint256[] memory tokensToTest = new uint256[](1);
39+
tokensToTest[0] = firstValidTokenId;
40+
token.mint(userB, firstValidTokenId, 1);
41+
assertFalse(validator.isValidDiscountRegistration(userA, abi.encode(tokensToTest)));
42+
}
43+
44+
function test_returnsFalse_whenTheUserHasAnInvalidToken() public {
45+
uint256[] memory tokensToTest = new uint256[](3);
46+
tokensToTest[0] = firstValidTokenId;
47+
tokensToTest[1] = secondValidTokenId;
48+
tokensToTest[2] = invalidTokenId;
49+
token.mint(userA, invalidTokenId, 1);
50+
assertFalse(validator.isValidDiscountRegistration(userA, abi.encode(tokensToTest)));
51+
}
52+
53+
function test_returnsFalseWhenTheUserHasTokenButProvidesWrongList() public {
54+
uint256[] memory tokensToTest = new uint256[](1);
55+
tokensToTest[0] = secondValidTokenId;
56+
token.mint(userA, firstValidTokenId, 1);
57+
assertFalse(validator.isValidDiscountRegistration(userA, abi.encode(tokensToTest)));
58+
}
59+
60+
function test_returnsFalseWhenUserProvidesEmptyList() public {
61+
uint256[] memory tokensToTest = new uint256[](0);
62+
token.mint(userA, firstValidTokenId, 1);
63+
assertFalse(validator.isValidDiscountRegistration(userA, abi.encode(tokensToTest)));
64+
}
65+
}

0 commit comments

Comments
 (0)