From 81646024b7624430048b69e2c639cc1446299c5a Mon Sep 17 00:00:00 2001 From: Kabie Date: Fri, 5 Jul 2024 05:12:01 +0800 Subject: [PATCH] add basic Product & ProductFactory --- .gitignore | 1 + foundry.toml | 3 +- remappings.txt | 3 +- script/Counter.s.sol | 12 -------- soldeer.lock | 12 ++++++-- src/Counter.sol | 14 --------- src/Product.sol | 35 +++++++++++++++++++++ src/ProductFactory.sol | 38 +++++++++++++++++++++++ test/Counter.t.sol | 24 --------------- test/ProductFactory.t.sol | 64 +++++++++++++++++++++++++++++++++++++++ 10 files changed, 151 insertions(+), 55 deletions(-) delete mode 100644 script/Counter.s.sol delete mode 100644 src/Counter.sol create mode 100644 src/Product.sol create mode 100644 src/ProductFactory.sol delete mode 100644 test/Counter.t.sol create mode 100644 test/ProductFactory.t.sol diff --git a/.gitignore b/.gitignore index 2037240..b1f810e 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ docs/ .env /dependencies +/.vscode diff --git a/foundry.toml b/foundry.toml index 5030866..7fc9245 100644 --- a/foundry.toml +++ b/foundry.toml @@ -8,6 +8,7 @@ test = "test" libs = ["dependencies"] [dependencies] +forge-std = { version = "1.9.1"} "@openzeppelin-contracts" = { version = "5.0.2" } -forge-std = { version = "1.8.2"} +"@openzeppelin-contracts-upgradeable" = { version = "5.0.2" } diff --git a/remappings.txt b/remappings.txt index cc0fbad..5eaee20 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,2 +1,3 @@ -forge-std=dependencies/forge-std-1.8.2/src +forge-std=dependencies/forge-std-1.9.1/src @openzeppelin/contracts=dependencies/@openzeppelin-contracts-5.0.2 +@openzeppelin/contracts-upgradeable=dependencies/@openzeppelin-contracts-upgradeable-5.0.2 diff --git a/script/Counter.s.sol b/script/Counter.s.sol deleted file mode 100644 index df9ee8b..0000000 --- a/script/Counter.s.sol +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import {Script, console} from "forge-std/Script.sol"; - -contract CounterScript is Script { - function setUp() public {} - - function run() public { - vm.broadcast(); - } -} diff --git a/soldeer.lock b/soldeer.lock index a493b24..8b4e81c 100644 --- a/soldeer.lock +++ b/soldeer.lock @@ -5,8 +5,14 @@ version = "5.0.2" source = "https://soldeer-revisions.s3.amazonaws.com/@openzeppelin-contracts/5_0_2_14-03-2024_06:11:59_contracts.zip" checksum = "8bc4f0acc7c187771b878d46f7de4bfad1acad2eb5d096d9d05d34035853f5c3" +[[dependencies]] +name = "@openzeppelin-contracts-upgradeable" +version = "5.0.2" +source = "https://soldeer-revisions.s3.amazonaws.com/@openzeppelin-contracts-upgradeable/5_0_2_14-03-2024_06:12:07_contracts-upgradeable.zip" +checksum = "fb3f8db8541fc01636f91b0e7d9dd6f450f1bf7e2b4a17e96caf6e779ace8f5b" + [[dependencies]] name = "forge-std" -version = "1.8.2" -source = "https://soldeer-revisions.s3.amazonaws.com/forge-std/1_8_2_19-05-2024_18:52:07_forge-std-1.8.2.zip" -checksum = "88a37e1d79f60b8aad08c7bd50a7a5ef973fc172b1495028d0725a17f5a4976c" +version = "1.9.1" +source = "https://soldeer-revisions.s3.amazonaws.com/forge-std/v1_9_1_03-07-2024_14:44:59_forge-std-v1.9.1.zip" +checksum = "110b35ad3604d91a919c521c71206c18cd07b29c750bd90b5cbbaf37288c9636" diff --git a/src/Counter.sol b/src/Counter.sol deleted file mode 100644 index aded799..0000000 --- a/src/Counter.sol +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -contract Counter { - uint256 public number; - - function setNumber(uint256 newNumber) public { - number = newNumber; - } - - function increment() public { - number++; - } -} diff --git a/src/Product.sol b/src/Product.sol new file mode 100644 index 0000000..9b04adc --- /dev/null +++ b/src/Product.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {ERC721Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol"; +import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; + +contract Product is Initializable, ERC721Upgradeable, OwnableUpgradeable { + string public baseURI; + + constructor() { + _disableInitializers(); + } + + function initialize(address initialOwner, string calldata name, string calldata symbol) public initializer { + __ERC721_init(name, symbol); + __Ownable_init(initialOwner); + } + + function setBaseURI(string calldata baseURI_) external onlyOwner { + baseURI = baseURI_; + } + + function _baseURI() internal view override returns (string memory) { + return baseURI; + } + + function mint(address to, uint256 tokenId) public onlyOwner { + _mint(to, tokenId); + } + + function safeMint(address to, uint256 tokenId) public onlyOwner { + _safeMint(to, tokenId); + } +} diff --git a/src/ProductFactory.sol b/src/ProductFactory.sol new file mode 100644 index 0000000..890a8e6 --- /dev/null +++ b/src/ProductFactory.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {Product} from "./Product.sol"; + +contract ProductFactory is Ownable { + address public product_template; + + constructor(address product_template_) Ownable(msg.sender) { + setProgramTemplate(product_template_); + } + + function setProgramTemplate(address product_template_) public onlyOwner { + requireIsContract(product_template_); + product_template = product_template_; + } + + function requireIsContract(address contract_address) internal view { + uint256 code_size; + assembly { + code_size := extcodesize(contract_address) + } + require(code_size > 0); + } + + function createProduct(string calldata name, string calldata symbol) external returns (Product product) { + address product_address = _createProduct(product_template, msg.sender, name); + product = Product(product_address); + product.initialize(msg.sender, name, symbol); + } + + function _createProduct(address implementation, address vendor, string calldata name) internal returns (address) { + bytes32 salt = keccak256(abi.encodePacked("DePHY_ID", vendor, name)); + return Clones.cloneDeterministic(implementation, salt); + } +} diff --git a/test/Counter.t.sol b/test/Counter.t.sol deleted file mode 100644 index 54b724f..0000000 --- a/test/Counter.t.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import {Test, console} from "forge-std/Test.sol"; -import {Counter} from "../src/Counter.sol"; - -contract CounterTest is Test { - Counter public counter; - - function setUp() public { - counter = new Counter(); - counter.setNumber(0); - } - - function test_Increment() public { - counter.increment(); - assertEq(counter.number(), 1); - } - - function testFuzz_SetNumber(uint256 x) public { - counter.setNumber(x); - assertEq(counter.number(), x); - } -} diff --git a/test/ProductFactory.t.sol b/test/ProductFactory.t.sol new file mode 100644 index 0000000..94c68f9 --- /dev/null +++ b/test/ProductFactory.t.sol @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {Test, console} from "forge-std/Test.sol"; +import {Product} from "../src/Product.sol"; +import {ProductFactory} from "../src/ProductFactory.sol"; + +contract ProductFactoryTest is Test { + ProductFactory product_factory; + Product template_product; + Product test_product; + + address factory_owner; + address product_owner; + + function setUp() public { + factory_owner = vm.createWallet(0x42).addr; + + template_product = new Product(); + + vm.prank(factory_owner); + product_factory = new ProductFactory(address(template_product)); + assertEq(product_factory.owner(), factory_owner); + + product_owner = vm.createWallet(0x99).addr; + vm.prank(product_owner); + test_product = product_factory.createProduct("Test Product", "TEST"); + } + + function test_product_template() public { + assertEq(product_factory.product_template(), address(template_product)); + + Product product2 = new Product(); + vm.prank(factory_owner); + product_factory.setProgramTemplate(address(product2)); + assertEq(product_factory.product_template(), address(product2)); + } + + function test_create_product() public { + test_product = product_factory.createProduct("Test Product", "TEST"); + + assertEq(test_product.name(), "Test Product"); + assertEq(test_product.symbol(), "TEST"); + } + + function test_mint_product() public { + assertEq(test_product.owner(), product_owner); + + vm.prank(product_owner); + test_product.mint(msg.sender, 2024); + assertEq(test_product.ownerOf(2024), msg.sender); + + assertEq(test_product.tokenURI(2024), ""); + vm.prank(product_owner); + test_product.setBaseURI("https://example.com/"); + assertEq(test_product.tokenURI(2024), "https://example.com/2024"); + } + + function testFuzz_mint_product(address tokenOwner, uint256 tokenId) public { + vm.prank(product_owner); + test_product.mint(tokenOwner, tokenId); + assertEq(test_product.ownerOf(tokenId), tokenOwner); + } +}