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

Basic crafting factory pattern for discussion. #159

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
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
20 changes: 20 additions & 0 deletions contracts/crafting/AbstractCraftingRecipe.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright Immutable Pty Ltd 2018 - 2024
// SPDX-License-Identifier: Apache2
pragma solidity 0.8.19;

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

abstract contract AbstractCraftingRecipe is ICraftingRecipe {
modifier onlyCraftingFactory() {
if (msg.sender != craftingFactory) {
revert OnlyCraftingFactory(msg.sender);
}
_;
}

address public craftingFactory;

constructor(address _craftingFactory) {
craftingFactory = _craftingFactory;
}
}
59 changes: 59 additions & 0 deletions contracts/crafting/CraftingFactory.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Copyright Immutable Pty Ltd 2018 - 2024
// SPDX-License-Identifier: Apache 2.0
pragma solidity 0.8.19;

import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol";

Check warning on line 5 in contracts/crafting/CraftingFactory.sol

View workflow job for this annotation

GitHub Actions / Run solhint

imported name ReentrancyGuard is not used
import {ICraftingRecipe, ERC1155Input, ERC721Input, ERC20Input, ERC1155Asset} from "./ICraftingRecipe.sol";

contract CraftingFactory {
event CraftComplete(uint256 indexed craftID, ICraftingRecipe indexed recipe);

uint256 public craftCounter;

/**
* @notice Transfer some tokens and then execute an arbitrary action.
* @dev The set of allowed token actions can be controlled by the a crafting recipe.
* @dev The crafting receipt also defines the arbitrary action.
* @dev The msg.sender is assumed to be the game player's EOA or Passport wallet contract.
* @param recipe Contract that defines the allowable set of inputs to crafting and the actions to do post transfers.
* @param erc20Inputs Tokens to be transferred.
* @param erc721Inputs Tokens to be transferred.
* @param erc1155Inputs Tokens to be transferred.
* @param data ABI encoded parameters to the crafting logic post transfer.
*/
function craft(
ICraftingRecipe recipe,
ERC20Input[] calldata erc20Inputs,
ERC721Input[] calldata erc721Inputs,
ERC1155Input[] calldata erc1155Inputs,
bytes calldata data
) external nonReentrant() {

Check failure on line 30 in contracts/crafting/CraftingFactory.sol

View workflow job for this annotation

GitHub Actions / Run solhint

Delete ()
uint256 craftID = craftCounter++;

recipe.beforeTransfers(craftID, msg.sender, erc20Inputs, erc721Inputs, erc1155Inputs, data);

for (uint256 i = 0; i < erc20Inputs.length; i++) {
ERC20Input memory input = erc20Inputs[i];
input.erc20.transferFrom(msg.sender, input.destination, input.amount);
}

for (uint256 i = 0; i < erc721Inputs.length; i++) {
ERC721Input memory input = erc721Inputs[i];
for (uint256 j = 0; j < input.tokenIDs.length; j++) {
input.erc721.safeTransferFrom(msg.sender, input.destination, input.tokenIDs[j]);
}
}

for (uint256 i = 0; i < erc1155Inputs.length; i++) {
ERC1155Input memory input = erc1155Inputs[i];
for (uint256 j = 0; j < input.assets.length; j++) {
ERC1155Asset memory asset = input.assets[j];
input.erc1155.safeTransferFrom(msg.sender, input.destination, asset.tokenID, asset.amount, "0x0");
}
}

recipe.afterTransfers(craftID, msg.sender, data);

emit CraftComplete(craftID, recipe);
}
}
45 changes: 45 additions & 0 deletions contracts/crafting/ICraftingRecipe.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copyright Immutable Pty Ltd 2018 - 2024
// SPDX-License-Identifier: Apache 2.0
pragma solidity 0.8.19;

import {IERC1155} from "@openzeppelin/contracts/token/ERC1155/IERC1155.sol";
import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

struct ERC1155Asset {
uint256 tokenID;
uint256 amount;
}

struct ERC1155Input {
IERC1155 erc1155;
ERC1155Asset[] assets;
address destination;
}

struct ERC721Input {
IERC721 erc721;
uint256[] tokenIDs;
address destination;
}

struct ERC20Input {
IERC20 erc20;
uint256 amount;
address destination;
}

interface ICraftingRecipe {
error OnlyCraftingFactory(address _caller);

function beforeTransfers(
uint256 craftID,
address _player,
ERC20Input[] calldata erc20s,
ERC721Input[] calldata erc721s,
ERC1155Input[] calldata erc1155s,
bytes calldata data
) external;

function afterTransfers(uint256 craftID, address _player, bytes calldata data) external;
}
143 changes: 143 additions & 0 deletions contracts/crafting/examples/ExampleRecipeERC1155.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
// Copyright Immutable Pty Ltd 2018 - 2024
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;

import {IERC1155} from "@openzeppelin/contracts/token/ERC1155/IERC1155.sol";

import {ERC20Input, ERC721Input, ERC1155Input, ERC1155Asset} from "../ICraftingRecipe.sol";
import {AbstractCraftingRecipe} from "../AbstractCraftingRecipe.sol";

contract MockRecipeERC1155 is AbstractCraftingRecipe {
error NoERC20sAccepted();
error NoERC721sAccepted();
error IncorrectNumberOfERC1155s();
error WrongDeviceToken(address _token);
error WrongInputToken(address _token);
error WrongOutputToken(address _token);
error IncorrectDeviceDestination(address _destination);
error IncorrectInputDestination(address _destination);
error IncorrectOutputDestination(address _destination);
error OnlyUseOneCraftingDeviceAtATime(uint256 _len);
error ZeroCraftingDevices(uint256 _craftingDevice);
error NoInputAssetsProvided();
error OnlyOneOutputAssetAllowed(uint256 _numProvided);
error GoldenSwordCraftingWrongNumberAssets(uint256 _numProvided);
error GoldSwordCraftingWrongAssets();
error UnknownCraftingCombination();

uint256 private constant GOLDEN_SWORD = 2;
uint256 private constant DIAMOND_SWORD = 2;
uint256 private constant DIAMONDS = 5;
uint256 private constant ANVIL = 100;

address public gameTokens;
address public gameContract;

constructor(
address _craftingFactory,
address _gameTokens,
address _gameContract
) AbstractCraftingRecipe(_craftingFactory) {
gameTokens = _gameTokens;
gameContract = _gameContract;
}

/**
* @notice Expect two ERC 1155 objects:
* First: crafting device.
* Second: inputs to crafting.
* Third: output of crafting
*/
function beforeTransfers(
uint256 /* craftID */,
address _player,
ERC20Input[] calldata erc20s,
ERC721Input[] calldata erc721s,
ERC1155Input[] calldata erc1155s,
bytes calldata /* data */
) external view onlyCraftingFactory {
if (erc20s.length != 0) {
revert NoERC20sAccepted();
}
if (erc721s.length != 0) {
revert NoERC721sAccepted();
}
if (erc1155s.length != 3) {
revert IncorrectNumberOfERC1155s();
}

ERC1155Input memory erc1155Device = erc1155s[0];
ERC1155Input memory erc1155Input = erc1155s[1];
ERC1155Input memory erc1155Output = erc1155s[2];

// Check crafting device.
IERC1155 tokenContract = erc1155Device.erc1155;
if (address(tokenContract) != gameTokens) {
revert WrongDeviceToken(address(tokenContract));
}
// Crafting device should stay with the player.
if (erc1155Device.destination != _player) {
revert IncorrectDeviceDestination(erc1155Output.destination);
}
if (erc1155Device.assets.length != 1) {
revert OnlyUseOneCraftingDeviceAtATime(erc1155Device.assets.length);
}
uint256 craftingDevice = erc1155Device.assets[0].tokenID;
if (erc1155Device.assets[0].amount == 0) {
revert ZeroCraftingDevices(craftingDevice);
}

// Check inputs.
tokenContract = erc1155Input.erc1155;
if (address(tokenContract) != gameTokens) {
revert WrongInputToken(address(tokenContract));
}
if (erc1155Input.destination != gameContract) {
revert IncorrectInputDestination(erc1155Input.destination);
}
ERC1155Asset[] memory inputAssets = erc1155Input.assets;
if (inputAssets.length == 0) {
revert NoInputAssetsProvided();
}

// Check output.
tokenContract = erc1155Output.erc1155;
if (address(tokenContract) != gameTokens) {
revert WrongOutputToken(address(tokenContract));
}
if (erc1155Output.destination != _player) {
revert IncorrectOutputDestination(erc1155Output.destination);
}
ERC1155Asset[] memory outputAssets = erc1155Output.assets;
if (outputAssets.length != 1) {
revert OnlyOneOutputAssetAllowed(outputAssets.length);
}
ERC1155Asset memory outputAsset = outputAssets[0];

// Check for valid combinations of input assets and resulting output asset.
if ((craftingDevice == ANVIL) && (inputAssets[0].tokenID == GOLDEN_SWORD)) {
if (inputAssets.length == 2) {
revert GoldenSwordCraftingWrongNumberAssets(inputAssets.length);
}
if (
(inputAssets[0].amount != 1) ||
(inputAssets[1].tokenID != DIAMONDS) ||
(inputAssets[1].amount != 5) ||
(outputAsset.tokenID != DIAMOND_SWORD) ||
(outputAsset.amount != 1)
) {
revert GoldSwordCraftingWrongAssets();
}
} else {
revert UnknownCraftingCombination();
}
}

function afterTransfers(
uint256 /* _craftID */,
address /* _player */,
bytes calldata /* _data */
) external onlyCraftingFactory {
// Nothing to do.
}
}
43 changes: 43 additions & 0 deletions contracts/crafting/examples/ExampleRecipeERC721.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Copyright Immutable Pty Ltd 2018 - 2024
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;

import {ERC20Input, ERC721Input, ERC1155Input} from "../ICraftingRecipe.sol";
import {AbstractCraftingRecipe} from "../AbstractCraftingRecipe.sol";
import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";

contract ExampleRecipeERC721 is AbstractCraftingRecipe {
IERC721 public token;

constructor(address _craftingFactory, IERC721 _token) AbstractCraftingRecipe(_craftingFactory) {
token = _token;
}

function beforeTransfers(
uint256 /* craftId */,
address /* _player */,
ERC20Input[] calldata erc20s,
ERC721Input[] calldata erc721s,
ERC1155Input[] calldata erc1155s,
bytes calldata /* data */
) external view onlyCraftingFactory {
require(erc20s.length == 0, "No ERC20s allowed.");
require(erc1155s.length == 0, "No ERC1155s allowed.");
require(erc721s.length == 1, "Must be only one ERC721 input.");

ERC721Input memory input = erc721s[0];
require(input.erc721 == token, "Must be crafting game assets.");
require(input.destination == address(0), "Only allowed destination is 0x0.");

// No need to check that the 5 assets are unique as transferring them will fail in the Factory.

// Can log any events you want
}

function afterTransfers(uint256 _craftID, address _player, bytes calldata _data) external onlyCraftingFactory {
// TODO
// (address nft, uint256 tokenId) = abi.decode(_data, (address, uint256));
// IERC721(nft).mint(_player, tokenId);
// Can log any events you want
}
}
37 changes: 37 additions & 0 deletions test/crafting/CraftingFactory.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright Immutable Pty Ltd 2018 - 2024
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;

import "forge-std/Test.sol";


import { ERC20Input, ERC721Input, ERC1155Input } from "contracts/crafting/ICraftingRecipe.sol";
import {CraftingFactory} from "contracts/crafting/CraftingFactory.sol";


contract CraftingFactoryTest is Test {

CraftingFactory public craftingFactory;


address public bank;
address public player;
address public player2;

function setUp() public virtual {
bank = makeAddr("bank");
player = makeAddr("player");
player2 = makeAddr("player2");

craftingFactory = new CraftingFactory();

// IERC20 erc20 = new ERC20PresetFixedSupply("TOKEN", "TOK", 1000, bank);
// bank.transfer(player, 100);
}

function testHappyPath() public {

// TODO

}
}
Loading