Skip to content

Commit 0185fbe

Browse files
committed
ERC721Wrapper uses Receiver
1 parent 5f0fd78 commit 0185fbe

File tree

8 files changed

+339
-27
lines changed

8 files changed

+339
-27
lines changed

audits/2023_04_Wrappers_Ignacio_Mazzara.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,9 @@ The rest of the contracts in the repositories are assumed to be audited.
3232

3333
## General Notes
3434

35-
The _ERC1155FloorWrapper_ wraps and unwraps tokens using the `onERC1155Received` and `onERC1155BatchReceived` functions, but the _ERC721FloorWrapper_ uses specific `deposit` and `withdraw` functions with no support to `onERC721Received`. Using different strategies for the same outcome makes the contract harder to follow and costly for the users in the case of the _ERC721FloorWrapper_. They need an extra transaction to `approve` each token or an `approvalForAll` before depositing. Consider using the same strategy for both wrappers.
35+
The _ERC1155FloorWrapper_ wraps and unwraps tokens using the `onERC1155Received` and `onERC1155BatchReceived` functions, but the _ERC721FloorWrapper_ uses specific `deposit` and `withdraw` functions with no support to `onERC721Received`. Using different strategies for the same outcome makes the contract harder to follow and costly for the users in the case of the _ERC721FloorWrapper_. They need an extra transaction to `approve` each token or an `approvalForAll` before depositing. Consider using the same strategy for both wrappers. **ADDRESSED: Updated ERC721FloorWrapper to use receiver functions. Have kept deposit to enable multiple deposits in a single transaction.**
3636

37-
Another thing to have in mind is that the users can claim unwrapped tokens if someone, by mistake, doesn't use the wrappers as expected. This has been addressed before the audit started[here by the team](https://github.com/0xsequence/niftyswap/pull/79#pullrequestreview-1362003788)
37+
Another thing to have in mind is that the users can claim unwrapped tokens if someone, by mistake, doesn't use the wrappers as expected. This has been addressed before the audit started[here by the team](https://github.com/0xsequence/niftyswap/pull/79#pullrequestreview-1362003788). **IGNORED: Intentional, as stated.**
3838

3939
## Interfaces
4040

src/contracts/interfaces/IERC721FloorWrapper.sol

Lines changed: 59 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,79 @@
22
pragma solidity ^0.8.4;
33

44
import {IERC1155} from "@0xsequence/erc-1155/contracts/interfaces/IERC1155.sol";
5+
import {IERC1155TokenReceiver} from "@0xsequence/erc-1155/contracts/interfaces/IERC1155TokenReceiver.sol";
6+
import {IERC721Receiver} from "./IERC721Receiver.sol";
57

6-
interface IERC721FloorWrapper is IERC1155 {
8+
interface IERC721FloorWrapper is IERC1155, IERC1155TokenReceiver, IERC721Receiver {
79
event TokensDeposited(uint256[] tokenIds);
810

911
event TokensWithdrawn(uint256[] tokenIds);
1012

13+
struct DepositRequestObj {
14+
address recipient;
15+
bytes data;
16+
}
17+
18+
struct WithdrawRequestObj {
19+
uint256[] tokenIds;
20+
address recipient;
21+
bytes data;
22+
}
23+
24+
/**
25+
* Accepts ERC-721 tokens to wrap.
26+
* @param _operator The address which called `safeTransferFrom` function.
27+
* @param _from The address which previously owned the token.
28+
* @param _tokenId The ID of the token being transferred.
29+
* @param _data Additional data formatted as DepositRequestObj.
30+
* @return `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`
31+
* @notice This is the preferred method for wrapping a single token.
32+
*/
33+
function onERC721Received(address _operator, address _from, uint256 _tokenId, bytes calldata _data)
34+
external
35+
returns (bytes4);
36+
1137
/**
1238
* Deposit and wrap ERC-721 tokens.
1339
* @param _tokenIds The ERC-721 token ids to deposit.
1440
* @param _recipient The recipient of the wrapped tokens.
1541
* @param _data Data to pass to ERC-1155 receiver.
1642
* @notice Users must first approve this contract address on the ERC-721 contract.
17-
* @dev This contract intentionally does not support IERC721Receiver for gas optimisations.
43+
* @notice This function can wrap multiple ERC-721 tokens at once.
1844
*/
1945
function deposit(uint256[] calldata _tokenIds, address _recipient, bytes calldata _data) external;
2046

2147
/**
22-
* Unwrap and withdraw ERC-721 tokens.
23-
* @param _tokenIds The ERC-721 token ids to withdraw.
24-
* @param _recipient The recipient of the unwrapped tokens.
25-
* @param _data Data to pass to ERC-1155 receiver.
48+
* Accepts wrapped ERC-1155 tokens to unwrap.
49+
* @param _operator The address which called `safeTransferFrom` function.
50+
* @param _from The address which previously owned the token.
51+
* @param _tokenId The ID of the token being transferred.
52+
* @param _amount The amount of tokens being transferred.
53+
* @param _data Additional data formatted as WithdrawRequestObj.
54+
* @return `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)`
55+
*/
56+
function onERC1155Received(
57+
address _operator,
58+
address _from,
59+
uint256 _tokenId,
60+
uint256 _amount,
61+
bytes calldata _data
62+
) external returns (bytes4);
63+
64+
/**
65+
* Accepts wrapped ERC-1155 tokens to unwrap.
66+
* @param _operator The address which called `safeTransferFrom` function.
67+
* @param _from The address which previously owned the token.
68+
* @param _tokenIds The IDs of the tokens being transferred.
69+
* @param _amounts The amounts of tokens being transferred.
70+
* @param _data Additional data formatted as WithdrawRequestObj.
71+
* @return `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))`
2672
*/
27-
function withdraw(uint256[] calldata _tokenIds, address _recipient, bytes calldata _data) external;
73+
function onERC1155BatchReceived(
74+
address _operator,
75+
address _from,
76+
uint256[] calldata _tokenIds,
77+
uint256[] calldata _amounts,
78+
bytes calldata _data
79+
) external returns (bytes4);
2880
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
pragma solidity ^0.8.4;
3+
4+
interface IERC721Receiver {
5+
function onERC721Received(address operator, address from, uint256 tokenId, bytes calldata data)
6+
external
7+
returns (bytes4);
8+
}

src/contracts/utils/WrapperErrors.sol

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ abstract contract WrapperErrors {
88
// Factories
99
error WrapperCreationFailed(address tokenAddr);
1010

11-
// ERC1155
11+
// Wrappers
12+
error InvalidERC721Received();
1213
error InvalidERC1155Received();
1314
error InvalidDepositRequest();
1415
error InvalidWithdrawRequest();

src/contracts/wrappers/ERC1155FloorWrapper.sol

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ contract ERC1155FloorWrapper is IERC1155FloorWrapper, ERC1155MintBurn, IERC1155M
4747
revert UnsupportedMethod();
4848
}
4949

50+
//
51+
// Tokens
52+
//
53+
5054
/**
5155
* Accepts ERC-1155 tokens to wrap and wrapped ERC-1155 tokens to unwrap.
5256
* @param _id The ID of the token being transferred.
@@ -168,6 +172,10 @@ contract ERC1155FloorWrapper is IERC1155FloorWrapper, ERC1155MintBurn, IERC1155M
168172
emit TokensWithdrawn(obj.tokenIds, obj.tokenAmounts);
169173
}
170174

175+
//
176+
// Views
177+
//
178+
171179
/**
172180
* A distinct Uniform Resource Identifier (URI) for a given token.
173181
* @param _id The token id.

src/contracts/wrappers/ERC721FloorWrapper.sol

Lines changed: 95 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {ERC1155MintBurn} from "@0xsequence/erc-1155/contracts/tokens/ERC1155/ERC
1111
import {WrapperErrors} from "../utils/WrapperErrors.sol";
1212

1313
import {IERC721} from "../interfaces/IERC721.sol";
14-
import {IERC721FloorWrapper} from "../interfaces/IERC721FloorWrapper.sol";
14+
import {IERC721FloorWrapper, IERC1155TokenReceiver, IERC721Receiver} from "../interfaces/IERC721FloorWrapper.sol";
1515

1616
/**
1717
* @notice Allows users to wrap any amount of a ERC-721 token with a 1:1 ratio.
@@ -51,15 +51,43 @@ contract ERC721FloorWrapper is IERC721FloorWrapper, ERC1155MintBurn, IERC1155Met
5151
}
5252

5353
//
54-
// Tokens
54+
// Deposit
5555
//
5656

57+
/**
58+
* Accepts ERC-721 tokens to wrap.
59+
* @param _tokenId The ID of the token being transferred.
60+
* @param _data Additional data formatted as DepositRequestObj.
61+
* @return `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`
62+
* @notice This is the preferred method for wrapping a single token.
63+
*/
64+
function onERC721Received(address, address, uint256 _tokenId, bytes calldata _data) external returns (bytes4) {
65+
if (msg.sender != address(token)) {
66+
revert InvalidERC721Received();
67+
}
68+
DepositRequestObj memory obj;
69+
(obj) = abi.decode(_data, (DepositRequestObj));
70+
if (obj.recipient == address(0)) {
71+
// Don't allow deposits to the zero address
72+
revert InvalidWithdrawRequest();
73+
}
74+
75+
uint256[] memory tokenIds = new uint256[](1);
76+
tokenIds[0] = _tokenId;
77+
emit TokensDeposited(tokenIds);
78+
79+
_mint(obj.recipient, TOKEN_ID, 1, obj.data);
80+
81+
return IERC721Receiver.onERC721Received.selector;
82+
}
83+
5784
/**
5885
* Deposit and wrap ERC-721 tokens.
5986
* @param _tokenIds The ERC-721 token ids to deposit.
6087
* @param _recipient The recipient of the wrapped tokens.
6188
* @param _data Data to pass to ERC-1155 receiver.
6289
* @notice Users must first approve this contract address on the ERC-721 contract.
90+
* @notice This function can wrap multiple ERC-721 tokens at once.
6391
*/
6492
function deposit(uint256[] calldata _tokenIds, address _recipient, bytes calldata _data) external {
6593
if (_recipient == address(0)) {
@@ -79,22 +107,78 @@ contract ERC721FloorWrapper is IERC721FloorWrapper, ERC1155MintBurn, IERC1155Met
79107
_mint(_recipient, TOKEN_ID, length, _data);
80108
}
81109

110+
//
111+
// Withdraw
112+
//
113+
114+
/**
115+
* Accepts wrapped ERC-1155 tokens to unwrap.
116+
* @param _amount The amount of tokens being transferred.
117+
* @param _data Additional data with no specified format.
118+
* @return `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)`
119+
*/
120+
function onERC1155Received(address, address, uint256, uint256 _amount, bytes calldata _data)
121+
external
122+
returns (bytes4)
123+
{
124+
if (msg.sender != address(this)) {
125+
revert InvalidERC1155Received();
126+
}
127+
_withdraw(_amount, _data);
128+
129+
return IERC1155TokenReceiver.onERC1155Received.selector;
130+
}
131+
132+
/**
133+
* Accepts wrapped ERC-1155 tokens to unwrap.
134+
* @param _ids The IDs of the tokens being transferred.
135+
* @param _amounts The amounts of tokens being transferred.
136+
* @param _data Additional data formatted as WithdrawRequestObj.
137+
* @return `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))`
138+
*/
139+
function onERC1155BatchReceived(
140+
address,
141+
address,
142+
uint256[] calldata _ids,
143+
uint256[] calldata _amounts,
144+
bytes calldata _data
145+
) public returns (bytes4) {
146+
if (msg.sender != address(this)) {
147+
revert InvalidERC1155Received();
148+
}
149+
150+
if (_ids.length != 1) {
151+
revert InvalidERC1155Received();
152+
}
153+
// Either ids[0] == TOKEN_ID or amounts[0] == 0
154+
_withdraw(_amounts[0], _data);
155+
156+
return IERC1155TokenReceiver.onERC1155BatchReceived.selector;
157+
}
158+
82159
/**
83160
* Unwrap and withdraw ERC-721 tokens.
84-
* @param _tokenIds The ERC-721 token ids to withdraw.
85-
* @param _recipient The recipient of the unwrapped tokens.
86-
* @param _data Data to pass to ERC-721 receiver.
161+
* @param _amount The amount of ERC-1155 tokens recieved.
162+
* @param _data Additional data formatted as WithdrawRequestObj.
87163
*/
88-
function withdraw(uint256[] calldata _tokenIds, address _recipient, bytes calldata _data) external {
89-
if (_recipient == address(0)) {
164+
function _withdraw(uint256 _amount, bytes calldata _data) private {
165+
WithdrawRequestObj memory obj;
166+
(obj) = abi.decode(_data, (WithdrawRequestObj));
167+
if (obj.recipient == address(0)) {
168+
// Don't allow deposits to the zero address
169+
revert InvalidWithdrawRequest();
170+
}
171+
172+
uint256 length = obj.tokenIds.length;
173+
if (_amount != length) {
174+
// The amount of tokens received must match the amount of tokens being withdrawn
90175
revert InvalidWithdrawRequest();
91176
}
92177

93-
uint256 length = _tokenIds.length;
94178
_burn(msg.sender, TOKEN_ID, length);
95-
emit TokensWithdrawn(_tokenIds);
179+
emit TokensWithdrawn(obj.tokenIds);
96180
for (uint256 i; i < length;) {
97-
token.safeTransferFrom(address(this), _recipient, _tokenIds[i], _data);
181+
token.safeTransferFrom(address(this), obj.recipient, obj.tokenIds[i], obj.data);
98182
unchecked {
99183
// Can never overflow
100184
i++;
@@ -103,7 +187,7 @@ contract ERC721FloorWrapper is IERC721FloorWrapper, ERC1155MintBurn, IERC1155Met
103187
}
104188

105189
//
106-
// Metadata
190+
// Views
107191
//
108192

109193
/**

tests_foundry/ERC1155FloorWrapper.test.sol

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,10 @@ contract ERC1155FloorWrapperTest is TestHelperBase, WrapperErrors {
278278
erc1155.safeBatchTransferFrom(USER, wrapperAddr, tokenIds, tokenAmounts, encodeDepositRequest(address(0), ""));
279279
}
280280

281+
//
282+
// Deposit with Batch Receiver
283+
//
284+
281285
//
282286
// Withdraw
283287
//
@@ -456,6 +460,10 @@ contract ERC1155FloorWrapperTest is TestHelperBase, WrapperErrors {
456460
);
457461
}
458462

463+
//
464+
// Withdraw with Batch Receiver
465+
//
466+
459467
//
460468
// Transfers
461469
//

0 commit comments

Comments
 (0)