Skip to content

Commit 7e649dd

Browse files
authored
first iteration of cross-burn contract (#104)
* first iteration of cross-burn contract * lint-fix * remove pricing * emit BurnEvent properly * remove nonce from cross-burn * emit redeem amount * rename CrossBurn to CrossChainBurn * add tests for crossChainBurn contract * add checks for redeemAmount and tests
1 parent d0e3a26 commit 7e649dd

File tree

5 files changed

+1376
-0
lines changed

5 files changed

+1376
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,361 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.0;
4+
5+
/// @author: manifold.xyz
6+
7+
import "@manifoldxyz/libraries-solidity/contracts/access/AdminControl.sol";
8+
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
9+
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
10+
import "@openzeppelin/contracts/token/ERC1155/IERC1155.sol";
11+
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
12+
import ".././libraries/manifold-membership/IManifoldMembership.sol";
13+
import "./ICrossChainBurn.sol";
14+
import "./Interfaces.sol";
15+
16+
contract CrossChainBurn is ICrossChainBurn, ReentrancyGuard, AdminControl {
17+
using ECDSA for bytes32;
18+
19+
address private _signingAddress;
20+
mapping(uint256 => mapping(address => mapping(uint256 => bool))) private _usedTokens;
21+
mapping(uint256 => uint64) private _totalCount;
22+
23+
constructor(address initialOwner, address signingAddress) {
24+
_transferOwnership(initialOwner);
25+
_signingAddress = signingAddress;
26+
}
27+
28+
function supportsInterface(bytes4 interfaceId) public view virtual override(AdminControl, IERC165) returns (bool) {
29+
return
30+
interfaceId == type(ICrossChainBurn).interfaceId ||
31+
interfaceId == type(AdminControl).interfaceId ||
32+
interfaceId == type(IERC1155Receiver).interfaceId ||
33+
interfaceId == type(IERC721Receiver).interfaceId ||
34+
super.supportsInterface(interfaceId);
35+
}
36+
37+
/**
38+
* @dev See {ICrossChainBurn-withdraw}.
39+
*/
40+
function withdraw(address payable recipient, uint256 amount) external override adminRequired {
41+
_forwardValue(recipient, amount);
42+
}
43+
44+
/**
45+
* @dev See {ICrossChainBurn-updateSigner}.
46+
*/
47+
function updateSigner(address signingAddress) external override adminRequired {
48+
_signingAddress = signingAddress;
49+
}
50+
51+
/**
52+
* @dev See {ICrossChainBurn-recover}.
53+
*/
54+
function recover(address tokenAddress, uint256 tokenId, address destination) external override adminRequired {
55+
IERC721(tokenAddress).transferFrom(address(this), destination, tokenId);
56+
}
57+
58+
/**
59+
* @dev See {ICrossChainBurn-burnRedeem}.
60+
*/
61+
function burnRedeem(BurnSubmission calldata submission) external payable override nonReentrant {
62+
if (!_isAvailable(submission)) revert InsufficientSupply();
63+
_validateSubmission(submission);
64+
_burnTokens(submission.instanceId, msg.sender, submission.burnTokens);
65+
_redeem(msg.sender, submission);
66+
}
67+
68+
/**
69+
* @dev See {ICrossChainBurn-burnRedeem}.
70+
*/
71+
function burnRedeem(BurnSubmission[] calldata submissions) external payable override nonReentrant {
72+
for (uint256 i; i < submissions.length; ) {
73+
BurnSubmission calldata submission = submissions[i];
74+
if (_isAvailable(submission)) {
75+
// Only validate if the variation requested available
76+
_validateSubmission(submission);
77+
_burnTokens(submission.instanceId, msg.sender, submission.burnTokens);
78+
_redeem(msg.sender, submission);
79+
}
80+
unchecked {
81+
++i;
82+
}
83+
}
84+
}
85+
86+
function _validateSubmission(BurnSubmission memory submission) private view {
87+
if (block.timestamp > submission.expiration) revert ExpiredSignature();
88+
if (submission.redeemAmount == 0) revert InvalidInput();
89+
90+
// Verify valid message based on input variables
91+
bytes32 expectedMessage = keccak256(
92+
abi.encode(
93+
submission.instanceId,
94+
submission.burnTokens,
95+
submission.redeemAmount,
96+
submission.totalLimit,
97+
submission.expiration
98+
)
99+
);
100+
address signer = submission.message.recover(submission.signature);
101+
if (submission.message != expectedMessage || signer != _signingAddress) revert InvalidSignature();
102+
}
103+
104+
function _isAvailable(BurnSubmission memory submission) private view returns (bool) {
105+
// Check total limit
106+
if (
107+
submission.totalLimit > 0 && (_totalCount[submission.instanceId] + submission.redeemAmount) > submission.totalLimit
108+
) {
109+
return false;
110+
}
111+
112+
return true;
113+
}
114+
115+
function _burnTokens(uint256 instanceId, address from, BurnToken[] calldata burnTokens) private {
116+
for (uint256 i; i < burnTokens.length; ) {
117+
BurnToken memory burnToken = burnTokens[i];
118+
_burn(instanceId, from, burnToken);
119+
unchecked {
120+
++i;
121+
}
122+
}
123+
}
124+
125+
/**
126+
* Helper to burn token
127+
*/
128+
function _burn(uint256 instanceId, address from, BurnToken memory burnToken) private {
129+
if (burnToken.tokenSpec == TokenSpec.ERC1155) {
130+
if (burnToken.burnSpec == BurnSpec.NONE) {
131+
// Send to 0xdEaD to burn if contract doesn't have burn function
132+
IERC1155(burnToken.contractAddress).safeTransferFrom(from, address(0xdEaD), burnToken.tokenId, burnToken.amount, "");
133+
} else if (burnToken.burnSpec == BurnSpec.MANIFOLD) {
134+
// Burn using the creator core's burn function
135+
uint256[] memory tokenIds = new uint256[](1);
136+
tokenIds[0] = burnToken.tokenId;
137+
uint256[] memory amounts = new uint256[](1);
138+
amounts[0] = burnToken.amount;
139+
Manifold1155(burnToken.contractAddress).burn(from, tokenIds, amounts);
140+
} else if (burnToken.burnSpec == BurnSpec.OPENZEPPELIN) {
141+
// Burn using OpenZeppelin's burn function
142+
OZBurnable1155(burnToken.contractAddress).burn(from, burnToken.tokenId, burnToken.amount);
143+
} else {
144+
revert InvalidBurnSpec();
145+
}
146+
} else if (burnToken.tokenSpec == TokenSpec.ERC721) {
147+
if (burnToken.amount != 1) revert InvalidToken(burnToken.contractAddress, burnToken.tokenId);
148+
if (burnToken.burnSpec == BurnSpec.NONE) {
149+
// Send to 0xdEaD to burn if contract doesn't have burn function
150+
IERC721(burnToken.contractAddress).safeTransferFrom(from, address(0xdEaD), burnToken.tokenId, "");
151+
} else if (burnToken.burnSpec == BurnSpec.MANIFOLD || burnToken.burnSpec == BurnSpec.OPENZEPPELIN) {
152+
if (from != address(this)) {
153+
// 721 `burn` functions do not have a `from` parameter, so we must verify the owner
154+
if (IERC721(burnToken.contractAddress).ownerOf(burnToken.tokenId) != from) {
155+
revert TransferFailure();
156+
}
157+
}
158+
// Burn using the contract's burn function
159+
Burnable721(burnToken.contractAddress).burn(burnToken.tokenId);
160+
} else {
161+
revert InvalidBurnSpec();
162+
}
163+
} else if (burnToken.tokenSpec == TokenSpec.ERC721_NO_BURN) {
164+
if (from != address(this)) {
165+
// 721 `burn` functions do not have a `from` parameter, so we must verify the owner
166+
if (IERC721(burnToken.contractAddress).ownerOf(burnToken.tokenId) != from) {
167+
revert TransferFailure();
168+
}
169+
}
170+
// Make sure token hasn't previously been used
171+
if (_usedTokens[instanceId][burnToken.contractAddress][burnToken.tokenId]) {
172+
revert InvalidToken(burnToken.contractAddress, burnToken.tokenId);
173+
}
174+
// Mark token as used
175+
_usedTokens[instanceId][burnToken.contractAddress][burnToken.tokenId] = true;
176+
} else {
177+
revert InvalidTokenSpec();
178+
}
179+
}
180+
181+
/**
182+
* @dev See {IERC721Receiver-onERC721Received}.
183+
*/
184+
function onERC721Received(
185+
address,
186+
address from,
187+
uint256 id,
188+
bytes calldata data
189+
) external override nonReentrant returns (bytes4) {
190+
_onERC721Received(from, id, data);
191+
return this.onERC721Received.selector;
192+
}
193+
194+
/**
195+
* @dev See {IERC1155Receiver-onERC1155Received}.
196+
*/
197+
function onERC1155Received(
198+
address,
199+
address from,
200+
uint256 id,
201+
uint256 value,
202+
bytes calldata data
203+
) external override nonReentrant returns (bytes4) {
204+
// Do burn redeem
205+
_onERC1155Received(from, id, value, data);
206+
return this.onERC1155Received.selector;
207+
}
208+
209+
/**
210+
* @dev See {IERC1155Receiver-onERC1155BatchReceived}.
211+
*/
212+
function onERC1155BatchReceived(
213+
address,
214+
address from,
215+
uint256[] calldata ids,
216+
uint256[] calldata values,
217+
bytes calldata data
218+
) external override nonReentrant returns (bytes4) {
219+
// Do burn redeem
220+
_onERC1155BatchReceived(from, ids, values, data);
221+
return this.onERC1155BatchReceived.selector;
222+
}
223+
224+
/**
225+
* @notice ERC721 token transfer callback
226+
* @param from the person sending the tokens
227+
* @param id the token id of the burn token
228+
* @param data bytes indicating the target burnRedeem and, optionally, a merkle proof that the token is valid
229+
*/
230+
function _onERC721Received(address from, uint256 id, bytes calldata data) private {
231+
BurnSubmission memory submission = abi.decode(data, (BurnSubmission));
232+
233+
// A single ERC721 can only be sent in directly for a burn if:
234+
// 1. The burn only requires one NFT (one burnToken)
235+
if (submission.burnTokens.length != 1) {
236+
revert InvalidInput();
237+
}
238+
if (!_isAvailable(submission)) revert InsufficientSupply();
239+
_validateSubmission(submission);
240+
241+
// Check that the burn token is valid
242+
BurnToken memory burnToken = submission.burnTokens[0];
243+
244+
// Can only take in one burn item
245+
if (burnToken.tokenSpec != TokenSpec.ERC721) {
246+
revert InvalidInput();
247+
}
248+
if (burnToken.contractAddress != msg.sender || burnToken.tokenId != id || burnToken.amount != 1) {
249+
revert InvalidToken(burnToken.contractAddress, burnToken.tokenId);
250+
}
251+
252+
// Do burn and redeem
253+
_burn(submission.instanceId, address(this), burnToken);
254+
_redeem(from, submission);
255+
}
256+
257+
/**
258+
* Execute onERC1155Received burn/redeem
259+
*/
260+
function _onERC1155Received(address from, uint256 tokenId, uint256 value, bytes calldata data) private {
261+
BurnSubmission memory submission = abi.decode(data, (BurnSubmission));
262+
263+
// A single 1155 can only be sent in directly for a burn if:
264+
// 1. The burn only requires one NFT (one burnToken)
265+
266+
if (submission.burnTokens.length != 1) {
267+
revert InvalidInput();
268+
}
269+
if (!_isAvailable(submission)) revert InsufficientSupply();
270+
_validateSubmission(submission);
271+
272+
// Check that the burn token is valid
273+
BurnToken memory burnToken = submission.burnTokens[0];
274+
275+
// Can only take in one burn item
276+
if (burnToken.tokenSpec != TokenSpec.ERC1155) {
277+
revert InvalidInput();
278+
}
279+
if (burnToken.contractAddress != msg.sender || burnToken.tokenId != tokenId) {
280+
revert InvalidToken(burnToken.contractAddress, burnToken.tokenId);
281+
}
282+
if (burnToken.amount != value) {
283+
revert InvalidBurnAmount();
284+
}
285+
286+
// Do burn and redeem
287+
_burn(submission.instanceId, address(this), burnToken);
288+
_redeem(from, submission);
289+
}
290+
291+
/**
292+
* Execute onERC1155BatchReceived burn/redeem
293+
*/
294+
function _onERC1155BatchReceived(
295+
address from,
296+
uint256[] calldata tokenIds,
297+
uint256[] calldata values,
298+
bytes calldata data
299+
) private {
300+
BurnSubmission memory submission = abi.decode(data, (BurnSubmission));
301+
302+
// A single 1155 can only be sent in directly for a burn if:
303+
// 1. We have the right data length
304+
if (submission.burnTokens.length != tokenIds.length) {
305+
revert InvalidInput();
306+
}
307+
if (!_isAvailable(submission)) revert InsufficientSupply();
308+
_validateSubmission(submission);
309+
310+
// Verify the values match what is needed and burn tokens
311+
for (uint256 i; i < submission.burnTokens.length; ) {
312+
BurnToken memory burnToken = submission.burnTokens[i];
313+
if (burnToken.contractAddress != msg.sender || burnToken.tokenId != tokenIds[i]) {
314+
revert InvalidToken(burnToken.contractAddress, burnToken.tokenId);
315+
}
316+
if (burnToken.amount != values[i]) {
317+
revert InvalidBurnAmount();
318+
}
319+
_burn(submission.instanceId, address(this), burnToken);
320+
unchecked {
321+
++i;
322+
}
323+
}
324+
325+
// Do redeem
326+
_redeem(from, submission);
327+
}
328+
329+
/**
330+
* Helper to perform the redeem
331+
*/
332+
function _redeem(address redeemer, BurnSubmission memory submission) private {
333+
// Increment total count
334+
_totalCount[submission.instanceId] += submission.redeemAmount;
335+
// Emit redeem event
336+
emit CrossChainBurn(
337+
submission.instanceId,
338+
redeemer,
339+
submission.redeemContract,
340+
submission.redeemNetworkId,
341+
submission.redeemAmount
342+
);
343+
}
344+
345+
/**
346+
* Send funds to receiver
347+
*/
348+
function _forwardValue(address payable receiver, uint256 amount) private {
349+
(bool sent, ) = receiver.call{ value: amount }("");
350+
if (!sent) {
351+
revert TransferFailure();
352+
}
353+
}
354+
355+
/**
356+
* @dev See {ICrossChainBurn-getTotalCount}.
357+
*/
358+
function getTotalCount(uint256 instanceId) external view override returns (uint64) {
359+
return _totalCount[instanceId];
360+
}
361+
}

0 commit comments

Comments
 (0)