Skip to content

Commit

Permalink
Add burn to mint contract for ERC1155
Browse files Browse the repository at this point in the history
  • Loading branch information
ScreamingHawk committed Nov 6, 2024
1 parent eb3a86d commit 4c57817
Show file tree
Hide file tree
Showing 2 changed files with 329 additions and 0 deletions.
196 changes: 196 additions & 0 deletions src/tokens/ERC1155/utility/burnToMint/ERC1155BurnToMint.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.19;

import {IERC1155} from "@0xsequence/erc-1155/contracts/interfaces/IERC1155.sol";
import {IERC1155TokenReceiver} from "@0xsequence/erc-1155/contracts/interfaces/IERC1155TokenReceiver.sol";
import {IERC1155ItemsFunctions} from "@0xsequence/contracts-library/tokens/ERC1155/presets/items/IERC1155Items.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";

// Thrown when token id is invalid
error InvalidTokenId();

// Thrown when mint requirements are not met
error MintRequirementsNotMet();

// Thrown when input array length is invalid
error InvalidArrayLength();

// Thrown when method called is invalid
error InvalidMethod();

interface IERC1155Items is IERC1155ItemsFunctions, IERC1155 {
function batchBurn(uint256[] memory tokenIds, uint256[] memory amounts) external;
}

struct TokenRequirements {
uint256 tokenId;
uint256 amount;
}

contract ERC1155BurnToMint is IERC1155TokenReceiver, Ownable {
IERC1155Items private immutable ITEMS;

Check warning on line 31 in src/tokens/ERC1155/utility/burnToMint/ERC1155BurnToMint.sol

View workflow job for this annotation

GitHub Actions / Solidity lint

Variable name must be in mixedCase

mapping(uint256 => TokenRequirements[]) public burnRequirements;
mapping(uint256 => TokenRequirements[]) public holdRequirements;

constructor(address items, address owner_) {
Ownable.transferOwnership(owner_);
ITEMS = IERC1155Items(items);
}

/**
* Contract owner can mint anything
*/
function mintOpen(address to, uint256 tokenId, uint256 amount) external onlyOwner {
ITEMS.mint(to, tokenId, amount, "");
}

/**
* Owner sets minting requirements for a token.
* @dev This function does not validate inputs ids of the inputs.
* @dev `burnTokenIds` and `holdTokenIds` should not overlap, should be unique and should not contain `mintTokenId`.
*/
function setMintRequirements(
uint256 mintTokenId,
uint256[] calldata burnTokenIds,
uint256[] calldata burnAmounts,
uint256[] calldata holdTokenIds,
uint256[] calldata holdAmounts
)
external
onlyOwner
{
if (burnTokenIds.length != burnAmounts.length || holdTokenIds.length != holdAmounts.length) {
revert InvalidArrayLength();
}

delete burnRequirements[mintTokenId];
delete holdRequirements[mintTokenId];
for (uint256 i = 0; i < burnTokenIds.length; i++) {
burnRequirements[mintTokenId].push(TokenRequirements(burnTokenIds[i], burnAmounts[i]));
}
for (uint256 i = 0; i < holdTokenIds.length; i++) {
holdRequirements[mintTokenId].push(TokenRequirements(holdTokenIds[i], holdAmounts[i]));
}
}

/**
* @notice Use `onERC1155BatchReceived` instead.
*/
function onERC1155Received(address, address, uint256, uint256, bytes calldata)
external
pure
override
returns (bytes4)
{
revert InvalidMethod();
}

/**
* Receive tokens for burning and mint new token.
* @dev `data` is abi.encode(mintTokenId).
*/
function onERC1155BatchReceived(
address,
address from,
uint256[] calldata tokenIds,
uint256[] calldata amounts,
bytes calldata data
)
external
override
returns (bytes4)
{
if (msg.sender != address(ITEMS)) {
// Got tokens from incorrect contract
revert MintRequirementsNotMet();
}

// Check mint requirements
uint256 mintTokenId = abi.decode(data, (uint256));
_checkMintRequirements(from, mintTokenId, tokenIds, amounts);

// Burn these tokens and mint the new token
ITEMS.batchBurn(tokenIds, amounts);
ITEMS.mint(from, mintTokenId, 1, "");

return this.onERC1155BatchReceived.selector;
}

/**
* Checks mint requirements for a token.
* @dev This function assumes the `burnTokenIds` and `burnAmounts` have been burned.
*/
function _checkMintRequirements(
address holder,
uint256 mintTokenId,
uint256[] calldata burnTokenIds,
uint256[] calldata burnAmounts
)
internal
view
{
if (burnTokenIds.length != burnAmounts.length || burnTokenIds.length == 0) {
revert InvalidArrayLength();
}

// Check burn tokens sent is correct
TokenRequirements[] memory requirements = burnRequirements[mintTokenId];
if (requirements.length != burnTokenIds.length) {
revert MintRequirementsNotMet();
}
for (uint256 i = 0; i < requirements.length; i++) {
if (requirements[i].tokenId != burnTokenIds[i] || requirements[i].amount != burnAmounts[i]) {
// Invalid burn token id or amount
revert MintRequirementsNotMet();
}
}

// Check held tokens
requirements = holdRequirements[mintTokenId];
if (requirements.length != 0) {
address[] memory holders = new address[](requirements.length);
uint256[] memory holdTokenIds = new uint256[](requirements.length);
for (uint256 i = 0; i < requirements.length; i++) {
holders[i] = holder;
holdTokenIds[i] = requirements[i].tokenId;
}
uint256[] memory balances = ITEMS.balanceOfBatch(holders, holdTokenIds);
for (uint256 i = 0; i < requirements.length; i++) {
if (balances[i] < requirements[i].amount) {
// Not enough held tokens
revert MintRequirementsNotMet();
}
}
}
}

function getMintRequirements(uint256 mintTokenId)
external
view
returns (
uint256[] memory burnIds,
uint256[] memory burnAmounts,
uint256[] memory holdIds,
uint256[] memory holdAmounts
)
{
TokenRequirements[] memory requirements = burnRequirements[mintTokenId];
uint256 requirementsLength = requirements.length;
burnIds = new uint256[](requirementsLength);
burnAmounts = new uint256[](requirementsLength);
for (uint256 i = 0; i < requirementsLength; i++) {
burnIds[i] = requirements[i].tokenId;
burnAmounts[i] = requirements[i].amount;
}

requirements = holdRequirements[mintTokenId];
requirementsLength = requirements.length;
holdIds = new uint256[](requirementsLength);
holdAmounts = new uint256[](requirementsLength);
for (uint256 i = 0; i < requirementsLength; i++) {
holdIds[i] = requirements[i].tokenId;
holdAmounts[i] = requirements[i].amount;
}
}
}
133 changes: 133 additions & 0 deletions test/tokens/ERC1155/utility/burnToMint/ERC1155BurnToMint.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.19;

import {TestHelper} from "../../../../TestHelper.sol";

import {ERC1155Items} from "src/tokens/ERC1155/presets/items/ERC1155Items.sol";
import {ERC1155BurnToMint, TokenRequirements} from "src/tokens/ERC1155/utility/burnToMint/ERC1155BurnToMint.sol";

contract ERC1155BurnToMintTest is TestHelper {
ERC1155Items private token;
ERC1155BurnToMint private minter;

address private owner;

function setUp() public {
owner = makeAddr("owner");

token = new ERC1155Items();
token.initialize(owner, "test", "ipfs://", "ipfs://", owner, 0);

minter = new ERC1155BurnToMint(address(token), owner);

// Set minter ro le on minter
vm.prank(owner);
token.grantRole(keccak256("MINTER_ROLE"), address(minter));
}

function testMintOpen(address holder, uint256 tokenId, uint256 amount) public {
assumeSafeAddress(holder);
amount = _bound(amount, 1, 100);

vm.label(holder, "holder");

vm.prank(owner);
minter.mintOpen(holder, tokenId, amount);
assertEq(token.balanceOf(holder, tokenId), amount);
}

function testBurnToMint(
address holder,
uint256 mintId,
uint256[] memory burnIds,
uint256[] memory burnAmounts,
uint256[] memory holdIds,
uint256[] memory holdAmounts
)
public
{
vm.assume(burnIds.length > 0); // At least one burn token
assumeSafeAddress(holder);
vm.label(holder, "holder");

(burnIds, burnAmounts) = _fixInputIdArray(burnIds, burnAmounts, 3, mintId);
(holdIds, holdAmounts) = _fixInputIdArray(holdIds, holdAmounts, 3, mintId);

vm.startPrank(owner);
// Set the burn requirements
minter.setMintRequirements(mintId, burnIds, burnAmounts, holdIds, holdAmounts);

// Mint required tokens for holding
token.batchMint(holder, holdIds, holdAmounts, "");
address[] memory _owners = new address[](burnIds.length);
for (uint256 i = 0; i < burnIds.length; i++) {
_owners[i] = holder;
}
uint256[] memory expectedEndBalances = token.balanceOfBatch(_owners, burnIds);

// Mint required tokens for burning
token.batchMint(holder, burnIds, burnAmounts, "");

vm.stopPrank();

// Send tokens for burning
bytes memory data = abi.encode(mintId);
vm.prank(holder);
token.safeBatchTransferFrom(holder, address(minter), burnIds, burnAmounts, data);

// Check minted tokens
assertEq(token.balanceOf(holder, mintId), 1);
// Check burned tokens
assertEq(token.balanceOfBatch(_owners, burnIds), expectedEndBalances);
}

function testGetMintRequirements(
uint256 mintId,
uint256[] memory burnIds,
uint256[] memory burnAmounts,
uint256[] memory holdIds,
uint256[] memory holdAmounts) public {
vm.assume(burnIds.length > 0); // At least one burn token

(burnIds, burnAmounts) = _fixInputIdArray(burnIds, burnAmounts, 3, mintId);
(holdIds, holdAmounts) = _fixInputIdArray(holdIds, holdAmounts, 3, mintId);

// Set the burn requirements
vm.prank(owner);
minter.setMintRequirements(mintId, burnIds, burnAmounts, holdIds, holdAmounts);

// Get mint requirements
(uint256[] memory burnIdsOut, uint256[] memory burnAmountsOut, uint256[] memory holdIdsOut, uint256[] memory holdAmountsOut) = minter.getMintRequirements(mintId);

assertEq(burnIdsOut.length, burnIds.length);
assertEq(burnAmountsOut.length, burnAmounts.length);
assertEq(holdIdsOut.length, holdIds.length);
assertEq(holdAmountsOut.length, holdAmounts.length);

for (uint256 i = 0; i < burnIds.length; i++) {
assertEq(burnIdsOut[i], burnIds[i]);
assertEq(burnAmountsOut[i], burnAmounts[i]);
}
for (uint256 i = 0; i < holdIds.length; i++) {
assertEq(holdIdsOut[i], holdIds[i]);
assertEq(holdAmountsOut[i], holdAmounts[i]);
}
}

function _fixInputIdArray(uint256[] memory idsInput, uint256[] memory amountsInput, uint256 size, uint256 noMatch) internal pure returns (uint256[] memory, uint256[] memory amounts) {
if (idsInput.length > size) {
assembly {
mstore(idsInput, size)
}
}
assumeNoDuplicates(idsInput);

amounts = new uint256[](idsInput.length);
for (uint256 i = 0; i < idsInput.length; i++) {
vm.assume(noMatch != idsInput[i]); // No matching this id
amounts[i] = _bound(amountsInput.length > i ? amountsInput[i] : 1, 1, 100); // Max size
}

return (idsInput, amounts);
}
}

0 comments on commit 4c57817

Please sign in to comment.