Skip to content
Open
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
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,3 +200,12 @@ forge script script/Deploy.s.sol:Deploy \
- Deploy to Whitechain Testnet
- Verify contracts
- Update README with deployed addresses and run instructions

## Deployed contracts (Whitechain Testnet, chainId 2625)

- ResourceNFT1155: `0xd117BAD3D1ACD4f6127f3eE9f1A0C4fD54c4C1fB`
- ItemNFT721: `0xDCc85a79c8AecE3B346e39D1e507742AB780bF0B`
- MagicToken: `0x92E318643B429dE531dd110206661383eE414Cc5`
- CraftingSearch: `0x463172b78Ed7db1c0f3B210A16Bd3b911e4fFF62`
- Marketplace: `0x0a32e3FB687401c11370F3C1adeD1c5384EF1E68`
- Admin (deployer):`0x305ACbb23d2244F491F3B1CFfe9e4F93e77c3A52`
225 changes: 225 additions & 0 deletions broadcast/Deploy.s.sol/2625/dry-run/run-1764634932546.json

Large diffs are not rendered by default.

225 changes: 225 additions & 0 deletions broadcast/Deploy.s.sol/2625/dry-run/run-latest.json

Large diffs are not rendered by default.

556 changes: 556 additions & 0 deletions broadcast/Deploy.s.sol/2625/run-1764635044433.json

Large diffs are not rendered by default.

556 changes: 556 additions & 0 deletions broadcast/Deploy.s.sol/2625/run-latest.json

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions foundry.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"lib/openzeppelin-contracts": {
"tag": {
"name": "v5.0.2",
"rev": "dbb6104ce834628e473d2173bbc9d47f81a9eec3"
}
}
}
1 change: 1 addition & 0 deletions script/Deploy.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ contract Deploy is Script {
res.grantRole(res.BURNER_ROLE(), address(cs));

items.grantRole(items.MINTER_ROLE(), address(cs));
items.grantRole(items.BURNER_ROLE(), address(mkt));

magic.grantRole(magic.MARKET_ROLE(), address(mkt));

Expand Down
82 changes: 78 additions & 4 deletions src/CraftingSearch.sol
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,93 @@ contract CraftingSearch is AccessControl {
// Optional constant for your cooldown if you need it later
uint256 public constant SEARCH_COOLDOWN = 60;

mapping(address => uint256) public lastSearchAt;
uint256 private randNonce;

struct Recipe {
uint256[] resourceIds;
uint256[] amounts;
}

mapping(uint256 => Recipe) private recipes;

constructor(address admin, ResourceNFT1155 _resources, ItemNFT721 _items) {
_grantRole(DEFAULT_ADMIN_ROLE, admin);
resources = _resources;
items = _items;
}

/// @notice TODO: implement resource search (cooldown + mintBatch on ResourceNFT1155).
function search() external pure {
revert("TODO: implement search()");
function search() external {
uint256 last = lastSearchAt[msg.sender];

if (last != 0) {
require(block.timestamp >= last + SEARCH_COOLDOWN, "cooldown");
}

lastSearchAt[msg.sender] = block.timestamp;

uint256[] memory ids = new uint256[](3);
uint256[] memory amounts = new uint256[](3);

for (uint256 i = 0; i < 3; i++) {
randNonce++;
uint256 rand = uint256(
keccak256(
abi.encodePacked(
msg.sender,
block.timestamp,
randNonce,
i
)
)
);

uint256 resourceId = (rand % 6) + 1; // [1..6]
ids[i] = resourceId;
amounts[i] = 1;
}

resources.mintBatch(msg.sender, ids, amounts);
}


function setRecipe(
uint256 itemType,
uint256[] memory resourceIds,
uint256[] memory amounts
) external onlyRole(DEFAULT_ADMIN_ROLE) {
require(resourceIds.length > 0, "empty recipe");
require(resourceIds.length == amounts.length, "len mismatch");

recipes[itemType].resourceIds = resourceIds;
recipes[itemType].amounts = amounts;
}

/// @notice TODO: implement crafting according to recipes (burnBatch + mintTo).
function craft(uint256 /*itemType*/) external pure {
revert("TODO: implement craft()");
function craft(uint256 itemType) external {
Recipe storage r = recipes[itemType];
uint256 len = r.resourceIds.length;
require(len > 0, "no recipe");

uint256[] memory ids = new uint256[](len);
uint256[] memory amounts = new uint256[](len);

for (uint256 i = 0; i < len; i++) {
ids[i] = r.resourceIds[i];
amounts[i] = r.amounts[i];
}

resources.burnBatch(msg.sender, ids, amounts);
items.mintTo(msg.sender);
}

function getRecipe(uint256 itemType)
external
view
returns (uint256[] memory resourceIds, uint256[] memory amounts)
{
Recipe storage r = recipes[itemType];
return (r.resourceIds, r.amounts);
}
}
8 changes: 7 additions & 1 deletion src/ItemNFT721.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,26 @@ import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";
*/
contract ItemNFT721 is ERC721, AccessControl {
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); // assign to CraftingSearch
bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE");

uint256 public nextId = 1;

constructor(address admin) ERC721("Cossack Items", "CITEM") {
_grantRole(DEFAULT_ADMIN_ROLE, admin);
}

/// @dev Template helper for future crafting (mint items).
function mintTo(
address to
) external onlyRole(MINTER_ROLE) returns (uint256) {
uint256 id = nextId++;
_safeMint(to, id);
return id;
}

function burn(uint256 tokenId) external onlyRole(BURNER_ROLE) {
_burn(tokenId);
}

function supportsInterface(
bytes4 interfaceId
) public view override(ERC721, AccessControl) returns (bool) {
Expand Down
34 changes: 28 additions & 6 deletions src/Marketplace.sol
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,43 @@ contract Marketplace is AccessControl {
ItemNFT721 public items;
MagicToken public magic;

struct Listing {
address seller;
uint256 price;
}

mapping(uint256 => Listing) public listings;

constructor(address admin, ItemNFT721 _items, MagicToken _magic) {
_grantRole(DEFAULT_ADMIN_ROLE, admin);
items = _items;
magic = _magic;
}

function list(uint256 /*tokenId*/, uint256 /*price*/) external pure {
revert("TODO: implement list()");
function list(uint256 tokenId, uint256 price) external {
require(items.ownerOf(tokenId) == msg.sender, "not owner");
require(price > 0, "price zero");
require(listings[tokenId].seller == address(0), "already listed");

listings[tokenId] = Listing({seller: msg.sender, price: price});
}

function delist(uint256 /*tokenId*/) external pure {
revert("TODO: implement delist()");
function delist(uint256 tokenId) external {
Listing memory l = listings[tokenId];
require(l.seller != address(0), "not listed");
require(l.seller == msg.sender, "not seller");

delete listings[tokenId];
}

function purchase(uint256 /*tokenId*/) external pure {
revert("TODO: implement purchase()");
function purchase(uint256 tokenId) external {
Listing memory l = listings[tokenId];
require(l.seller != address(0), "not listed");
require(items.ownerOf(tokenId) == l.seller, "owner changed");

delete listings[tokenId];

items.burn(tokenId);
magic.mint(l.seller, l.price);
}
}
110 changes: 106 additions & 4 deletions test/Template.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ contract TemplateTest is Test {
res.grantRole(res.MINTER_ROLE(), address(cs));
res.grantRole(res.BURNER_ROLE(), address(cs));
items.grantRole(items.MINTER_ROLE(), address(cs));
items.grantRole(items.BURNER_ROLE(), address(mkt));
magic.grantRole(magic.MARKET_ROLE(), address(mkt));
vm.stopPrank();
}
Expand All @@ -46,11 +47,112 @@ contract TemplateTest is Test {
assertTrue(res.hasRole(res.MINTER_ROLE(), address(cs)));
assertTrue(res.hasRole(res.BURNER_ROLE(), address(cs)));
assertTrue(items.hasRole(items.MINTER_ROLE(), address(cs)));
assertTrue(items.hasRole(items.BURNER_ROLE(), address(mkt)));
assertTrue(magic.hasRole(magic.MARKET_ROLE(), address(mkt)));
}

// TODO(student): add real tests as you implement features:
// - search() cooldown + 3 random ERC1155 mints
// - craft() recipes: burn ERC1155 + mint ERC721
// - marketplace listing + purchase: burn ERC721 + mint MAGIC to seller
// ===== helpers =====

function _totalResources(address player) internal view returns (uint256 total) {
// у нас 6 типів ресурсів із id 1..6
for (uint256 id = 1; id <= 6; id++) {
total += res.balanceOf(player, id);
}
}

// ===== search(): cooldown + 3 ERC1155 =====

function test_search_mints_three_and_enforces_cooldown() public {
address player = address(0xBEEF);

vm.prank(player);
cs.search();

assertEq(
_totalResources(player),
3,
"first search should mint 3 resources"
);

vm.prank(player);
vm.expectRevert("cooldown");
cs.search();

vm.warp(block.timestamp + cs.SEARCH_COOLDOWN() + 1);

vm.prank(player);
cs.search();

assertEq(
_totalResources(player),
6,
"after cooldown second search should mint +3"
);
}



// ===== craft(): burn 1155 + mint 721 =====

function test_craft_burns_resources_and_mints_item() public {
address player = address(0xB0B);

uint256 itemType = 1;

uint256[] memory rIds = new uint256[](3);
uint256[] memory rAmts = new uint256[](3);

rIds[0] = 1;
rIds[1] = 2;
rIds[2] = 3;
rAmts[0] = 1;
rAmts[1] = 1;
rAmts[2] = 1;

vm.prank(admin);
cs.setRecipe(itemType, rIds, rAmts);

vm.prank(address(cs));
res.mintBatch(player, rIds, rAmts);

assertEq(res.balanceOf(player, 1), 1);
assertEq(res.balanceOf(player, 2), 1);
assertEq(res.balanceOf(player, 3), 1);
assertEq(items.balanceOf(player), 0);

vm.prank(player);
cs.craft(itemType);

assertEq(res.balanceOf(player, 1), 0);
assertEq(res.balanceOf(player, 2), 0);
assertEq(res.balanceOf(player, 3), 0);

assertEq(items.balanceOf(player), 1, "player should receive crafted item");
}

// ===== marketplace: list + purchase (burn 721 + mint MAGIC) =====

function test_marketplace_list_and_purchase_burns_item_and_mints_magic() public {
address seller = address(0xCAFE);
address buyer = address(0xD00D);

vm.prank(address(cs));
uint256 tokenId = items.mintTo(seller);

uint256 price = 100e18;

vm.prank(seller);
mkt.list(tokenId, price);

vm.prank(buyer);
mkt.purchase(tokenId);

assertEq(items.balanceOf(seller), 0, "seller should not own the item after sale");

vm.expectRevert();
items.ownerOf(tokenId);

assertEq(magic.balanceOf(seller), price, "seller should receive MAGIC");
assertEq(magic.balanceOf(buyer), 0, "buyer should not receive MAGIC");
}
}