Skip to content

Commit 4fe67a2

Browse files
Merge pull request #40 from 0xsequence/packs
Add ERC1155Holder to prevent Packs.reveal abuse
2 parents d2fc7e0 + 9a9320b commit 4fe67a2

16 files changed

Lines changed: 887 additions & 309 deletions

File tree

.gas-snapshot

Lines changed: 269 additions & 233 deletions
Large diffs are not rendered by default.

script/Deploy.s.sol

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@ pragma solidity ^0.8.18;
33

44
import { PaymentCombiner } from "../src/payments/PaymentCombiner.sol";
55
import { PaymentsFactory } from "../src/payments/PaymentsFactory.sol";
6-
import { ERC1155ItemsFactory } from "../src/tokens/ERC1155/presets/items/ERC1155ItemsFactory.sol";
76

7+
import { ERC1155ItemsFactory } from "../src/tokens/ERC1155/presets/items/ERC1155ItemsFactory.sol";
88
import { ERC1155PackFactory } from "../src/tokens/ERC1155/presets/pack/ERC1155PackFactory.sol";
99
import { ERC1155SoulboundFactory } from "../src/tokens/ERC1155/presets/soulbound/ERC1155SoulboundFactory.sol";
10+
import { ERC1155Holder } from "../src/tokens/ERC1155/utility/holder/ERC1155Holder.sol";
1011
import { ERC1155SaleFactory } from "../src/tokens/ERC1155/utility/sale/ERC1155SaleFactory.sol";
12+
1113
import { ERC20ItemsFactory } from "../src/tokens/ERC20/presets/items/ERC20ItemsFactory.sol";
14+
1215
import { ERC721ItemsFactory } from "../src/tokens/ERC721/presets/items/ERC721ItemsFactory.sol";
1316
import { ERC721SoulboundFactory } from "../src/tokens/ERC721/presets/soulbound/ERC721SoulboundFactory.sol";
1417
import { ERC721SaleFactory } from "../src/tokens/ERC721/utility/sale/ERC721SaleFactory.sol";
@@ -67,9 +70,11 @@ contract Deploy is SingletonDeployer {
6770
salt,
6871
pk
6972
);
73+
address holder =
74+
_deployIfNotAlready("ERC1155Holder", abi.encodePacked(type(ERC1155Holder).creationCode), salt, pk);
7075
_deployIfNotAlready(
7176
"ERC1155PackFactory",
72-
abi.encodePacked(type(ERC1155PackFactory).creationCode, abi.encode(factoryOwner)),
77+
abi.encodePacked(type(ERC1155PackFactory).creationCode, abi.encode(factoryOwner, holder)),
7378
salt,
7479
pk
7580
);

src/tokens/ERC1155/ERC1155BaseToken.sol

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,7 @@ abstract contract ERC1155BaseToken is ERC1155Supply, ERC2981Controlled, SignalsI
2020
string public baseURI;
2121
string public contractURI;
2222

23-
/**
24-
* Deploy contract.
25-
*/
26-
constructor() { }
23+
bool private _initialized;
2724

2825
/**
2926
* Initialize the contract.
@@ -43,6 +40,10 @@ abstract contract ERC1155BaseToken is ERC1155Supply, ERC2981Controlled, SignalsI
4340
address implicitModeValidator,
4441
bytes32 implicitModeProjectId
4542
) internal {
43+
if (_initialized) {
44+
revert InvalidInitialization();
45+
}
46+
4647
name = tokenName;
4748
baseURI = tokenBaseURI;
4849
contractURI = tokenContractURI;
@@ -52,6 +53,8 @@ abstract contract ERC1155BaseToken is ERC1155Supply, ERC2981Controlled, SignalsI
5253
_grantRole(METADATA_ADMIN_ROLE, owner);
5354

5455
_initializeImplicitMode(owner, implicitModeValidator, implicitModeProjectId);
56+
57+
_initialized = true;
5558
}
5659

5760
//

src/tokens/ERC1155/extensions/supply/ERC1155Supply.sol

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,14 @@ abstract contract ERC1155Supply is ERC1155, IERC1155Supply {
4545

4646
uint256 nMint = _ids.length;
4747
uint256 totalAmount = 0;
48-
for (uint256 i = 0; i < nMint; i++) {
49-
totalAmount += _amounts[i];
50-
tokenSupply[_ids[i]] += _amounts[i];
48+
for (uint256 i; i < nMint;) {
49+
uint256 amount = _amounts[i];
50+
totalAmount += amount;
51+
tokenSupply[_ids[i]] += amount;
52+
unchecked {
53+
// Already checked in super._batchMint
54+
++i;
55+
}
5156
}
5257
totalSupply += totalAmount;
5358
}
@@ -76,9 +81,14 @@ abstract contract ERC1155Supply is ERC1155, IERC1155Supply {
7681

7782
uint256 nBurn = _ids.length;
7883
uint256 totalAmount = 0;
79-
for (uint256 i = 0; i < nBurn; i++) {
80-
tokenSupply[_ids[i]] -= _amounts[i];
81-
totalAmount += _amounts[i];
84+
for (uint256 i; i < nBurn;) {
85+
uint256 amount = _amounts[i];
86+
tokenSupply[_ids[i]] -= amount;
87+
totalAmount += amount;
88+
unchecked {
89+
// Already checked in super._batchBurn
90+
++i;
91+
}
8292
}
8393
totalSupply -= totalAmount;
8494
}

src/tokens/ERC1155/presets/items/ERC1155Items.sol

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,6 @@ contract ERC1155Items is ERC1155BaseToken, IERC1155Items {
1111

1212
bytes32 internal constant MINTER_ROLE = keccak256("MINTER_ROLE");
1313

14-
address private immutable initializer;
15-
bool private initialized;
16-
17-
constructor() {
18-
initializer = msg.sender;
19-
}
20-
2114
/**
2215
* Initialize the contract.
2316
* @param owner Owner address
@@ -40,18 +33,12 @@ contract ERC1155Items is ERC1155BaseToken, IERC1155Items {
4033
address implicitModeValidator,
4134
bytes32 implicitModeProjectId
4235
) public virtual {
43-
if (msg.sender != initializer || initialized) {
44-
revert InvalidInitialization();
45-
}
46-
4736
ERC1155BaseToken._initialize(
4837
owner, tokenName, tokenBaseURI, tokenContractURI, implicitModeValidator, implicitModeProjectId
4938
);
5039
_setDefaultRoyalty(royaltyReceiver, royaltyFeeNumerator);
5140

5241
_grantRole(MINTER_ROLE, owner);
53-
54-
initialized = true;
5542
}
5643

5744
//

src/tokens/ERC1155/presets/pack/ERC1155Pack.sol

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,20 @@ contract ERC1155Pack is ERC1155Items, IERC1155Pack {
1212

1313
bytes32 internal constant PACK_ADMIN_ROLE = keccak256("PACK_ADMIN_ROLE");
1414

15+
address public immutable erc1155Holder;
16+
1517
mapping(uint256 => bytes32) public merkleRoot;
1618
mapping(uint256 => uint256) public supply;
1719
mapping(uint256 => uint256) public remainingSupply;
1820

19-
mapping(uint256 => mapping(address => uint256)) internal _commitments;
21+
mapping(address => mapping(uint256 => uint256)) internal _commitments;
2022
mapping(uint256 => mapping(uint256 => uint256)) internal _availableIndices;
2123

22-
constructor() ERC1155Items() { }
24+
constructor(
25+
address _erc1155Holder
26+
) {
27+
erc1155Holder = _erc1155Holder;
28+
}
2329

2430
/// @inheritdoc ERC1155Items
2531
function initialize(
@@ -56,12 +62,11 @@ contract ERC1155Pack is ERC1155Items, IERC1155Pack {
5662
function commit(
5763
uint256 packId
5864
) external {
59-
if (_commitments[packId][msg.sender] != 0) {
65+
if (_commitments[msg.sender][packId] != 0) {
6066
revert PendingReveal();
6167
}
6268
_burn(msg.sender, packId, 1);
63-
uint256 revealAfterBlock = block.number + 1;
64-
_commitments[packId][msg.sender] = revealAfterBlock;
69+
_commitments[msg.sender][packId] = block.number + 1;
6570

6671
emit Commit(msg.sender, packId);
6772
}
@@ -80,21 +85,33 @@ contract ERC1155Pack is ERC1155Items, IERC1155Pack {
8085
revert InvalidProof();
8186
}
8287

83-
delete _commitments[packId][user];
88+
delete _commitments[user][packId];
8489
remainingSupply[packId]--;
8590

8691
// Point this index to the last index's value
8792
_availableIndices[packId][randomIndex] = _getIndexOrDefault(remainingSupply[packId], packId);
8893

89-
for (uint256 i = 0; i < packContent.tokenAddresses.length; i++) {
94+
for (uint256 i; i < packContent.tokenAddresses.length;) {
95+
address tokenAddr = packContent.tokenAddresses[i];
96+
uint256[] memory tokenIds = packContent.tokenIds[i];
9097
if (packContent.isERC721[i]) {
91-
for (uint256 j = 0; j < packContent.tokenIds[i].length; j++) {
92-
IERC721ItemsFunctions(packContent.tokenAddresses[i]).mint(user, packContent.tokenIds[i][j]);
98+
for (uint256 j; j < tokenIds.length;) {
99+
IERC721ItemsFunctions(tokenAddr).mint(user, tokenIds[j]);
100+
unchecked {
101+
++j;
102+
}
93103
}
94104
} else {
95-
IERC1155ItemsFunctions(packContent.tokenAddresses[i]).batchMint(
96-
user, packContent.tokenIds[i], packContent.amounts[i], ""
97-
);
105+
// Send via the holder fallback if available
106+
address to = user;
107+
if (erc1155Holder != address(0) && msg.sender != user) {
108+
to = erc1155Holder;
109+
}
110+
bytes memory packedData = abi.encode(user);
111+
IERC1155ItemsFunctions(tokenAddr).batchMint(to, tokenIds, packContent.amounts[i], packedData);
112+
}
113+
unchecked {
114+
++i;
98115
}
99116
}
100117

@@ -103,14 +120,14 @@ contract ERC1155Pack is ERC1155Items, IERC1155Pack {
103120

104121
/// @inheritdoc IERC1155Pack
105122
function refundPack(address user, uint256 packId) external {
106-
uint256 commitment = _commitments[packId][user];
123+
uint256 commitment = _commitments[user][packId];
107124
if (commitment == 0) {
108125
revert NoCommit();
109126
}
110127
if (uint256(blockhash(commitment)) != 0 || block.number <= commitment) {
111128
revert PendingReveal();
112129
}
113-
delete _commitments[packId][user];
130+
delete _commitments[user][packId];
114131
_mint(user, packId, 1, "");
115132
}
116133

@@ -125,7 +142,7 @@ contract ERC1155Pack is ERC1155Items, IERC1155Pack {
125142
revert AllPacksOpened();
126143
}
127144

128-
uint256 commitment = _commitments[packId][user];
145+
uint256 commitment = _commitments[user][packId];
129146
if (commitment == 0) {
130147
revert NoCommit();
131148
}
@@ -147,7 +164,7 @@ contract ERC1155Pack is ERC1155Items, IERC1155Pack {
147164
function supportsInterface(
148165
bytes4 interfaceId
149166
) public view override returns (bool) {
150-
return type(IERC1155Pack).interfaceId == interfaceId || super.supportsInterface(interfaceId);
167+
return interfaceId == type(IERC1155Pack).interfaceId || super.supportsInterface(interfaceId);
151168
}
152169

153170
}

src/tokens/ERC1155/presets/pack/ERC1155PackFactory.sol

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,10 @@ contract ERC1155PackFactory is IERC1155PackFactory, SequenceProxyFactory {
1313
/**
1414
* Creates an ERC-1155 Pack Factory.
1515
* @param factoryOwner The owner of the ERC-1155 Pack Factory
16+
* @param holderFallback The address of the ERC1155Holder fallback
1617
*/
17-
constructor(
18-
address factoryOwner
19-
) {
20-
ERC1155Pack impl = new ERC1155Pack();
18+
constructor(address factoryOwner, address holderFallback) {
19+
ERC1155Pack impl = new ERC1155Pack(holderFallback);
2120
SequenceProxyFactory._initialize(address(impl), factoryOwner);
2221
}
2322

src/tokens/ERC1155/presets/soulbound/ERC1155Soulbound.sol

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@ contract ERC1155Soulbound is ERC1155Items, IERC1155Soulbound {
1515

1616
bool internal _transferLocked;
1717

18-
constructor() ERC1155Items() { }
19-
2018
/// @inheritdoc ERC1155Items
2119
function initialize(
2220
address owner,
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
pragma solidity ^0.8.19;
3+
4+
import { IERC1155 } from "openzeppelin-contracts/contracts/token/ERC1155/IERC1155.sol";
5+
import { IERC1155Receiver, IERC165 } from "openzeppelin-contracts/contracts/token/ERC1155/IERC1155Receiver.sol";
6+
7+
/**
8+
* An ERC-1155 contract that allows permissive minting.
9+
*/
10+
contract ERC1155Holder is IERC1155Receiver {
11+
12+
/// @dev Emitted when a claim is added.
13+
event ClaimAdded(address claimant, address tokenAddress, uint256 tokenId, uint256 amount);
14+
/// @dev Emitted when a batch of claims is added.
15+
event ClaimAddedBatch(address claimant, address tokenAddress, uint256[] tokenIds, uint256[] amounts);
16+
17+
/// @dev Emitted when a claim is claimed.
18+
event Claimed(address claimant, address tokenAddress, uint256 tokenId, uint256 amount);
19+
/// @dev Emitted when a batch of claims is claimed.
20+
event ClaimedBatch(address claimant, address tokenAddress, uint256[] tokenIds, uint256[] amounts);
21+
22+
/// @dev Error thrown when the claimant is invalid.
23+
error InvalidClaimant();
24+
25+
/// @dev claimant -> tokenAddress -> tokenId -> amount
26+
mapping(address => mapping(address => mapping(uint256 => uint256))) public claims;
27+
28+
/// @dev Claims a token.
29+
/// @param claimant The claimant.
30+
/// @param tokenAddress The token address.
31+
/// @param tokenId The token id.
32+
function claim(address claimant, address tokenAddress, uint256 tokenId) public {
33+
uint256 amount = claims[claimant][tokenAddress][tokenId];
34+
delete claims[claimant][tokenAddress][tokenId];
35+
emit Claimed(claimant, tokenAddress, tokenId, amount);
36+
IERC1155(tokenAddress).safeTransferFrom(address(this), claimant, tokenId, amount, "");
37+
}
38+
39+
/// @dev Claims a batch of tokens.
40+
/// @param claimant The claimant.
41+
/// @param tokenAddress The token address.
42+
/// @param tokenIds The token ids.
43+
function claimBatch(address claimant, address tokenAddress, uint256[] memory tokenIds) public {
44+
uint256[] memory amounts = new uint256[](tokenIds.length);
45+
for (uint256 i = 0; i < tokenIds.length; i++) {
46+
amounts[i] = claims[claimant][tokenAddress][tokenIds[i]];
47+
delete claims[claimant][tokenAddress][tokenIds[i]];
48+
}
49+
emit ClaimedBatch(claimant, tokenAddress, tokenIds, amounts);
50+
IERC1155(tokenAddress).safeBatchTransferFrom(address(this), claimant, tokenIds, amounts, "");
51+
}
52+
53+
/// @inheritdoc IERC1155Receiver
54+
/// @param claimData The encoded claimant.
55+
function onERC1155Received(
56+
address,
57+
address,
58+
uint256 tokenId,
59+
uint256 amount,
60+
bytes calldata claimData
61+
) public virtual override returns (bytes4) {
62+
address claimant = _decodeClaimant(claimData);
63+
address tokenAddress = msg.sender;
64+
claims[claimant][tokenAddress][tokenId] += amount;
65+
emit ClaimAdded(claimant, tokenAddress, tokenId, amount);
66+
return this.onERC1155Received.selector;
67+
}
68+
69+
/// @inheritdoc IERC1155Receiver
70+
/// @param claimData The encoded claimant.
71+
function onERC1155BatchReceived(
72+
address,
73+
address,
74+
uint256[] calldata tokenIds,
75+
uint256[] calldata amounts,
76+
bytes calldata claimData
77+
) public virtual override returns (bytes4) {
78+
address claimant = _decodeClaimant(claimData);
79+
address tokenAddress = msg.sender;
80+
for (uint256 i = 0; i < tokenIds.length; i++) {
81+
claims[claimant][tokenAddress][tokenIds[i]] += amounts[i];
82+
}
83+
emit ClaimAddedBatch(claimant, tokenAddress, tokenIds, amounts);
84+
return this.onERC1155BatchReceived.selector;
85+
}
86+
87+
/// @dev Decodes the claimant from the claim data.
88+
function _decodeClaimant(
89+
bytes calldata claimData
90+
) internal pure returns (address claimant) {
91+
if (claimData.length == 20) {
92+
// Packed address format
93+
assembly {
94+
calldatacopy(0, claimData.offset, 20)
95+
claimant := shr(96, mload(0))
96+
}
97+
} else if (claimData.length == 32) {
98+
// ABI encoded address format
99+
(claimant) = abi.decode(claimData, (address));
100+
}
101+
if (claimant == address(0)) {
102+
revert InvalidClaimant();
103+
}
104+
return claimant;
105+
}
106+
107+
/// @inheritdoc IERC165
108+
function supportsInterface(
109+
bytes4 interfaceId
110+
) public view virtual override(IERC165) returns (bool) {
111+
return interfaceId == type(IERC1155Receiver).interfaceId || interfaceId == type(IERC165).interfaceId;
112+
}
113+
114+
}

0 commit comments

Comments
 (0)