Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Crafting v2 #239

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions contracts/crafting/Commands.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright Immutable Pty Ltd 2018 - 2023
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

library Commands {
enum CommandType {
ERC721Mint,
ERC721Burn,
ERC721Transfer,
ERC20Mint,
ERC20Transfer,
ERC1155Mint,
ERC1155Burn,
ERC1155Transfer
}

struct Command {
address token;
CommandType commandType;
bytes data;
}
}
33 changes: 33 additions & 0 deletions contracts/crafting/Crafting.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Copyright Immutable Pty Ltd 2018 - 2023
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import {Commands} from "./Commands.sol";
import {IERC721MintableBurnable} from "./IERC721MintableBurnable.sol";

contract Crafting {
event Crafted(bytes32 _craftId, address _sender, Commands.Command[] _commands, address _signer, uint256 _deadline);

function execute(
// bytes32 _craftId,
Commands.Command[] memory _commands
// address _signer,

Check failure on line 14 in contracts/crafting/Crafting.sol

View workflow job for this annotation

GitHub Actions / Run solhint

Replace ··· with )·external
// uint256 _deadline,

Check failure on line 15 in contracts/crafting/Crafting.sol

View workflow job for this annotation

GitHub Actions / Run solhint

Replace ········ with ····
// bytes calldata _signature

Check failure on line 16 in contracts/crafting/Crafting.sol

View workflow job for this annotation

GitHub Actions / Run solhint

Replace ········//·bytes·calldata·_signature⏎····)·external· with ····//·bytes·calldata·_signature
) external
{
for (uint256 i = 0; i < _commands.length; i++) {
Commands.Command memory command = _commands[i];
if (command.commandType == Commands.CommandType.ERC721Burn) {
uint256 tokenId = abi.decode(command.data, (uint256));
IERC721MintableBurnable(command.token).safeBurn(msg.sender, tokenId);
} else if (command.commandType == Commands.CommandType.ERC721Transfer) {
(address to, uint256 tokenId) = abi.decode(command.data, (address, uint256));
IERC721MintableBurnable(command.token).safeTransferFrom(msg.sender, to, tokenId);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In some scenarios, games would want to transfer to msg.sender instead.

} else if (command.commandType == Commands.CommandType.ERC721Mint) {
uint256 tokenId = abi.decode(command.data, (uint256));
IERC721MintableBurnable(command.token).safeMint(msg.sender, tokenId);
}
}
}
}
10 changes: 10 additions & 0 deletions contracts/crafting/IERC721MintableBurnable.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Copyright Immutable Pty Ltd 2018 - 2023
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

interface IERC721MintableBurnable {
function safeMint(address to, uint256 tokenId) external;
function burn(uint256 tokenId) external;
function safeBurn(address owner, uint256 tokenId) external;
function safeTransferFrom(address from, address to, uint256 tokenId) external;
}
35 changes: 35 additions & 0 deletions contracts/mocks/MockERC721MintableBurnable.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// SPDX-License-Identifier: MIT
// Compatible with OpenZeppelin Contracts ^5.0.0
pragma solidity ^0.8.20;

Check failure on line 3 in contracts/mocks/MockERC721MintableBurnable.sol

View workflow job for this annotation

GitHub Actions / Run solhint

Compiler version ^0.8.20 does not satisfy the 0.8.19 semver requirement

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";

Check warning on line 5 in contracts/mocks/MockERC721MintableBurnable.sol

View workflow job for this annotation

GitHub Actions / Run solhint

global import of path @openzeppelin/contracts/token/ERC721/ERC721.sol is not allowed. Specify names to import individually or bind all exports of the module into a name (import "path" as Name)
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Burnable.sol";

Check warning on line 6 in contracts/mocks/MockERC721MintableBurnable.sol

View workflow job for this annotation

GitHub Actions / Run solhint

global import of path @openzeppelin/contracts/token/ERC721/extensions/ERC721Burnable.sol is not allowed. Specify names to import individually or bind all exports of the module into a name (import "path" as Name)
import "@openzeppelin/contracts/access/AccessControl.sol";

Check warning on line 7 in contracts/mocks/MockERC721MintableBurnable.sol

View workflow job for this annotation

GitHub Actions / Run solhint

global import of path @openzeppelin/contracts/access/AccessControl.sol is not allowed. Specify names to import individually or bind all exports of the module into a name (import "path" as Name)

contract MockERC721MintableBurnable is ERC721, ERC721Burnable, AccessControl {
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");

error MismatchedTokenOwner();

constructor(address defaultAdmin, address minter) ERC721("MyToken", "MTK") {
_grantRole(DEFAULT_ADMIN_ROLE, defaultAdmin);
_grantRole(MINTER_ROLE, minter);
}

function safeMint(address to, uint256 tokenId) public onlyRole(MINTER_ROLE) {
_safeMint(to, tokenId);
}

function safeBurn(address owner, uint256 tokenId) external {
if (ownerOf(tokenId) != owner) {
revert MismatchedTokenOwner();
}
burn(tokenId);
}

// The following functions are overrides required by Solidity.

function supportsInterface(bytes4 interfaceId) public view override(ERC721, AccessControl) returns (bool) {
return super.supportsInterface(interfaceId);
}
}
117 changes: 117 additions & 0 deletions test/crafting/Crafting.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { expect } from "chai";
import { ethers } from "hardhat";
import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers";
import { Crafting, MockERC721MintableBurnable } from "../../typechain-types";

const commandTypeERC721Mint = 0;
const commandTypeERC721Burn = 1;
const commandTypeERC721Transfer = 2;

describe("Crafting", () => {
let owner: SignerWithAddress;
let user: SignerWithAddress;
let user2: SignerWithAddress;
let crafting: Crafting;
let erc721: MockERC721MintableBurnable;

beforeEach(async () => {
// Retrieve accounts
[owner, user, user2] = await ethers.getSigners();

crafting = await (await ethers.getContractFactory("Crafting")).deploy();

Check failure on line 21 in test/crafting/Crafting.test.ts

View workflow job for this annotation

GitHub Actions / Publish to NPM (dry run)

Property 'execute' is missing in type 'Contract' but required in type 'Crafting'.
erc721 = await (await ethers.getContractFactory("MockERC721MintableBurnable"))

Check failure on line 22 in test/crafting/Crafting.test.ts

View workflow job for this annotation

GitHub Actions / Publish to NPM (dry run)

Type 'Contract' is missing the following properties from type 'MockERC721MintableBurnable': DEFAULT_ADMIN_ROLE, MINTER_ROLE, approve, balanceOf, and 19 more.
.connect(owner)
.deploy(owner.address, owner.address);
});

describe("Contract Deployment", () => {
it("Should deploy the contract", async () => {
expect(crafting.address).to.not.be.undefined;
});
});

describe("Execute", () => {
describe("ERC721", () => {
it("Should burn item if approved", async () => {
await erc721.connect(owner).safeMint(user.address, 1);
await erc721.connect(user).approve(crafting.address, 1);
const commands = [
{
token: erc721.address,
commandType: commandTypeERC721Burn,
data: ethers.utils.defaultAbiCoder.encode(["uint256"], [1]),
},
];
expect(await crafting.connect(user).execute(commands)).to.not.reverted;
await expect(erc721.ownerOf(1)).to.be.reverted;
});

it("Should revert burn if not approved", async () => {
await erc721.connect(owner).safeMint(user.address, 1);
const commands = [
{
token: erc721.address,
commandType: commandTypeERC721Burn,
data: ethers.utils.defaultAbiCoder.encode(["uint256"], [1]),
},
];
await expect(crafting.connect(user).execute(commands)).to.be.revertedWith(
"ERC721: caller is not token owner or approved",
);
});

it("Should mint item if role is granted", async () => {
erc721.grantRole(await erc721.MINTER_ROLE(), crafting.address);
const commands = [
{
token: erc721.address,
commandType: commandTypeERC721Mint,
data: ethers.utils.defaultAbiCoder.encode(["uint256"], [1]),
},
];
expect(await crafting.connect(user).execute(commands)).to.not.reverted;
expect(await erc721.connect(user).ownerOf(1)).to.be.equal(user.address);
});

it("Should revert mint if role is not granted", async () => {
const commands = [
{
token: erc721.address,
commandType: commandTypeERC721Mint,
data: ethers.utils.defaultAbiCoder.encode(["uint256"], [1]),
},
];
await expect(crafting.connect(user).execute(commands)).to.be.reverted;
});

it("Should run multiple commands", async () => {
await erc721.connect(owner).safeMint(user.address, 1);
await erc721.connect(user).approve(crafting.address, 1);
await erc721.connect(owner).safeMint(user.address, 2);
await erc721.connect(user).approve(crafting.address, 2);
erc721.grantRole(await erc721.MINTER_ROLE(), crafting.address);
const commands = [
{
token: erc721.address,
commandType: commandTypeERC721Burn,
data: ethers.utils.defaultAbiCoder.encode(["uint256"], [1]),
},
{
token: erc721.address,
commandType: commandTypeERC721Transfer,
data: ethers.utils.defaultAbiCoder.encode(["address", "uint256"], [user2.address, 2]),
},
{
token: erc721.address,
commandType: commandTypeERC721Mint,
data: ethers.utils.defaultAbiCoder.encode(["uint256"], [3]),
},
];
expect(await crafting.connect(user).execute(commands)).to.not.reverted;
await expect(erc721.ownerOf(1)).to.be.reverted;
expect(await erc721.ownerOf(2)).to.be.equal(user2.address);
expect(await erc721.ownerOf(3)).to.be.equal(user.address);
});
});
});
});
Loading