├── .env.example ├── .github └── workflows │ └── test.yml ├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── foundry.toml ├── funding.json ├── remappings.txt ├── script ├── Deploy.s.sol ├── DeployAnimation.s.sol ├── DeployBrushEvents.sol ├── DeployMetadataRegistry.s.sol ├── DeployRewards.s.sol ├── DeploySubscription.s.sol ├── DeployWIP.s.sol └── deploy.sh ├── src ├── BasePaint.sol ├── BasePaintAnimation.sol ├── BasePaintBrush.sol ├── BasePaintBrushEvents.sol ├── BasePaintCollector.sol ├── BasePaintLoans.sol ├── BasePaintMetadataRegistry.sol ├── BasePaintRewards.sol ├── BasePaintSubscription.sol └── BasePaintWIP.sol └── test ├── BasePaint.t.sol ├── BasePaintAnimation.t.sol ├── BasePaintBrush.t.sol ├── BasePaintBrushEvents.t.sol ├── BasePaintMetadataRegistry.t.sol ├── BasePaintRewards.t.sol ├── BasePaintSubscription.t.sol └── BasePaintWIP.t.sol /.env.example: -------------------------------------------------------------------------------- 1 | DEPLOYER_KEY=0x________________________________________________________________ 2 | SIGNER_ADDRESS=0x________________________________________ 3 | OWNER_ADDRESS=0x________________________________________ 4 | 5 | ETHERSCAN_API_KEY=__________________________________ 6 | RPC_URL=__________________________________ -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: workflow_dispatch 4 | 5 | env: 6 | FOUNDRY_PROFILE: ci 7 | 8 | jobs: 9 | check: 10 | strategy: 11 | fail-fast: true 12 | 13 | name: Foundry project 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | with: 18 | submodules: recursive 19 | 20 | - name: Install Foundry 21 | uses: foundry-rs/foundry-toolchain@v1 22 | with: 23 | version: nightly 24 | 25 | - name: Run Forge build 26 | run: | 27 | forge --version 28 | forge build --sizes 29 | id: build 30 | 31 | - name: Run Forge tests 32 | run: | 33 | forge test -vvv 34 | id: test 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiler files 2 | cache/ 3 | out/ 4 | 5 | # Ignores development broadcast logs 6 | !/broadcast 7 | /broadcast/*/31337/ 8 | /broadcast/**/dry-run/ 9 | 10 | # Docs 11 | docs/ 12 | 13 | # Dotenv file 14 | .env 15 | .env.* 16 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/forge-std"] 2 | path = lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | [submodule "lib/openzeppelin-contracts"] 5 | path = lib/openzeppelin-contracts 6 | url = https://github.com/openzeppelin/openzeppelin-contracts 7 | [submodule "lib/openzeppelin-contracts-upgradeable"] 8 | path = lib/openzeppelin-contracts-upgradeable 9 | url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 w1nt3r.eth 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## BasePaint Contracts 2 | 3 | | Contract | Deployment Address | 4 | | ------------------------- | -------------------------------------------- | 5 | | BasePaint | `0xBa5e05cb26b78eDa3A2f8e3b3814726305dcAc83` | 6 | | BasePaintBrush | `0xD68fe5b53e7E1AbeB5A4d0A6660667791f39263a` | 7 | | BasePaintWIP | `0xE6249eAfdC9C8a809fE28a5213120B1860f9a75f` | 8 | | BasePaintRewards | `0xaff1A9E200000061fC3283455d8B0C7e3e728161` | 9 | | BasePaintBrushEvents | `0xb152f48F207d9D1C30Ff60d46E8cb8c1a5d00dEC` | 10 | | BasePaintAnimation | `0xC59F475122e914aFCf31C0a9E0A2274666135e4E` | 11 | | BasePaintMetadataRegistry | `0x5104482a2Ef3a03b6270D3e931eac890b86FaD01` | 12 | | BasePaintSubscription | `0x75CF063a65d361527180805b244bC51c1deAb075` | 13 | 14 | ## Usage 15 | 16 | ### Build 17 | 18 | ```shell 19 | $ forge build 20 | ``` 21 | 22 | ### Test 23 | 24 | ```shell 25 | $ forge test 26 | ``` 27 | 28 | ### Format 29 | 30 | ```shell 31 | $ forge fmt 32 | ``` 33 | 34 | ### Gas Snapshots 35 | 36 | ```shell 37 | $ forge snapshot 38 | ``` 39 | 40 | ### Anvil 41 | 42 | ```shell 43 | $ anvil 44 | ``` 45 | 46 | ### Deploy 47 | 48 | ```shell 49 | $ ./script/deploy.sh script/.s.sol 50 | ``` 51 | 52 | ### Cast 53 | 54 | ```shell 55 | $ cast 56 | ``` 57 | 58 | ### Help 59 | 60 | ```shell 61 | $ forge --help 62 | $ anvil --help 63 | $ cast --help 64 | ``` 65 | -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | src = "src" 3 | out = "out" 4 | libs = ["lib"] 5 | solc = "0.8.23" 6 | 7 | # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options 8 | 9 | optimizer = true 10 | optimizer_runs = 2000000 11 | -------------------------------------------------------------------------------- /funding.json: -------------------------------------------------------------------------------- 1 | { 2 | "opRetro": { 3 | "projectId": "0xc0901c8f9503f0bf91a1cca0c6c90e55af8719cc2237fb4a0bf0770b307cbd11" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /remappings.txt: -------------------------------------------------------------------------------- 1 | @openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/ 2 | @openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/ 3 | ds-test/=lib/forge-std/lib/ds-test/src/ 4 | erc4626-tests/=lib/openzeppelin-contracts-upgradeable/lib/erc4626-tests/ 5 | forge-std/=lib/forge-std/src/ 6 | openzeppelin-contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/ 7 | openzeppelin-contracts/=lib/openzeppelin-contracts/ 8 | -------------------------------------------------------------------------------- /script/Deploy.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import {Script, console2} from "forge-std/Script.sol"; 5 | import {Strings} from "openzeppelin-contracts/contracts/utils/Strings.sol"; 6 | import {BasePaint} from "../src/BasePaint.sol"; 7 | import {BasePaintBrush} from "../src/BasePaintBrush.sol"; 8 | 9 | contract CounterScript is Script { 10 | function setUp() public {} 11 | 12 | function run() public { 13 | address deployer = vm.rememberKey(vm.envUint("DEPLOYER_KEY")); 14 | address signer = vm.envAddress("SIGNER_ADDRESS"); 15 | address owner = vm.envAddress("OWNER_ADDRESS"); 16 | 17 | vm.startBroadcast(deployer); 18 | uint256 epochDuration = block.chainid == 8453 ? 1 days : 1 hours; 19 | BasePaintBrush brush = new BasePaintBrush(signer); 20 | BasePaint paint = new BasePaint(brush, epochDuration); 21 | 22 | if (block.chainid == 8453) { 23 | brush.transferOwnership(owner); 24 | paint.transferOwnership(owner); 25 | } else { 26 | brush.setBaseURI(string.concat("https://basepaint.xyz/api/brush/", Strings.toString(block.chainid), "/")); 27 | paint.setURI(string.concat("https://basepaint.xyz/api/art/", Strings.toString(block.chainid), "/{id}")); 28 | paint.setOpenEditionPrice(0.000026 ether); 29 | paint.start(); 30 | } 31 | 32 | vm.stopBroadcast(); 33 | 34 | console2.log("Brush", address(brush)); 35 | console2.log("Paint", address(paint)); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /script/DeployAnimation.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import {Script, console2} from "forge-std/Script.sol"; 5 | import {Strings} from "openzeppelin-contracts/contracts/utils/Strings.sol"; 6 | import {BasePaintAnimation} from "../src/BasePaintAnimation.sol"; 7 | 8 | contract DeployAnimationScript is Script { 9 | function setUp() public {} 10 | 11 | function run() public { 12 | address deployer = vm.rememberKey(vm.envUint("DEPLOYER_KEY")); 13 | address owner = vm.envAddress("OWNER_ADDRESS"); 14 | 15 | vm.startBroadcast(deployer); 16 | BasePaintAnimation animation = new BasePaintAnimation(); 17 | animation.transferOwnership(owner); 18 | 19 | vm.stopBroadcast(); 20 | 21 | console2.log("Animation", address(animation)); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /script/DeployBrushEvents.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import {Script, console2} from "forge-std/Script.sol"; 5 | import {BasePaintBrushEvents, IBasePaintBrush} from "../src/BasePaintBrushEvents.sol"; 6 | 7 | contract DeployBrushEventsScript is Script { 8 | function setUp() public {} 9 | 10 | function run() public { 11 | address deployer = vm.rememberKey(vm.envUint("DEPLOYER_KEY")); 12 | address brush = 0xD68fe5b53e7E1AbeB5A4d0A6660667791f39263a; 13 | 14 | vm.startBroadcast(deployer); 15 | BasePaintBrushEvents events = new BasePaintBrushEvents(IBasePaintBrush(brush)); 16 | vm.stopBroadcast(); 17 | 18 | console2.log("BasePaint Brush Events", address(events)); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /script/DeployMetadataRegistry.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.13; 3 | 4 | import "forge-std/Script.sol"; 5 | import "../src/BasePaintMetadataRegistry.sol"; 6 | import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; 7 | 8 | contract DeployMetadataRegistry is Script { 9 | function setUp() public {} 10 | 11 | function run() public { 12 | address deployer = vm.rememberKey(vm.envUint("DEPLOYER_KEY")); 13 | address owner = vm.envAddress("OWNER_ADDRESS"); 14 | address editor = vm.rememberKey(vm.envUint("ADMIN_KEY")); 15 | 16 | vm.startBroadcast(deployer); 17 | 18 | BasePaintMetadataRegistry implementation = new BasePaintMetadataRegistry(); 19 | bytes memory data = abi.encodeWithSelector(BasePaintMetadataRegistry.initialize.selector, owner, editor); 20 | ERC1967Proxy proxy = new ERC1967Proxy(address(implementation), data); 21 | 22 | vm.stopBroadcast(); 23 | 24 | console.log("MetadataRegistry Implementation deployed to:", address(implementation)); 25 | console.log("MetadataRegistry Proxy deployed to:", address(proxy)); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /script/DeployRewards.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import {Script, console2} from "forge-std/Script.sol"; 5 | import {BasePaintRewards, IBasePaint} from "../src/BasePaintRewards.sol"; 6 | 7 | contract DeployRewardsScript is Script { 8 | function setUp() public {} 9 | 10 | function run() public { 11 | address deployer = vm.rememberKey(vm.envUint("DEPLOYER_KEY")); 12 | address basepaint = 0xBa5e05cb26b78eDa3A2f8e3b3814726305dcAc83; 13 | address owner = vm.envAddress("OWNER_ADDRESS"); 14 | 15 | vm.startBroadcast(deployer); 16 | BasePaintRewards rewards = new BasePaintRewards(IBasePaint(basepaint), owner); 17 | vm.stopBroadcast(); 18 | 19 | console2.log("BasePaint Rewards", address(rewards)); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /script/DeploySubscription.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.17; 3 | 4 | import "forge-std/Script.sol"; 5 | import "../src/BasePaintSubscription.sol"; 6 | import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; 7 | 8 | contract DeployBasePaintSubscription is Script { 9 | function setUp() public {} 10 | 11 | function run() public { 12 | address deployer = vm.rememberKey(vm.envUint("DEPLOYER_KEY")); 13 | address owner = vm.envAddress("OWNER_ADDRESS"); 14 | address basePaintAddress = vm.envAddress("BASEPAINT_ADDRESS"); 15 | 16 | vm.startBroadcast(deployer); 17 | 18 | BasePaintSubscription implementation = new BasePaintSubscription(); 19 | bytes memory data = abi.encodeWithSelector(BasePaintSubscription.initialize.selector, basePaintAddress, owner); 20 | ERC1967Proxy proxy = new ERC1967Proxy(address(implementation), data); 21 | 22 | vm.stopBroadcast(); 23 | 24 | console.log("BasePaintSubscription Implementation deployed to:", address(implementation)); 25 | console.log("BasePaintSubscription Proxy deployed to:", address(proxy)); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /script/DeployWIP.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import {Script, console2} from "forge-std/Script.sol"; 5 | import {Strings} from "openzeppelin-contracts/contracts/utils/Strings.sol"; 6 | import {BasePaintWIP} from "../src/BasePaintWIP.sol"; 7 | 8 | contract DeployWIPScript is Script { 9 | function setUp() public {} 10 | 11 | function run() public { 12 | address deployer = vm.rememberKey(vm.envUint("DEPLOYER_KEY")); 13 | address signer = vm.envAddress("SIGNER_ADDRESS"); 14 | address owner = vm.envAddress("OWNER_ADDRESS"); 15 | 16 | vm.startBroadcast(deployer); 17 | BasePaintWIP wip = new BasePaintWIP(signer); 18 | wip.transferOwnership(owner); 19 | 20 | vm.stopBroadcast(); 21 | 22 | console2.log("WIP", address(wip)); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /script/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | set -o allexport 4 | source .env.local 5 | set +o allexport 6 | 7 | forge script "$1" -vvvv \ 8 | --rpc-url $RPC_URL \ 9 | --with-gas-price 10000000 \ 10 | --broadcast \ 11 | --verify 12 | -------------------------------------------------------------------------------- /src/BasePaint.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import {ERC1155} from "openzeppelin-contracts/contracts/token/ERC1155/ERC1155.sol"; 5 | import {Ownable} from "openzeppelin-contracts/contracts/access/Ownable.sol"; 6 | import {IBasePaintBrush} from "./BasePaintBrush.sol"; 7 | 8 | contract BasePaint is ERC1155("https://basepaint.xyz/api/art/{id}"), Ownable(msg.sender) { 9 | IBasePaintBrush public brushes; 10 | uint256 public immutable epochDuration; 11 | 12 | struct Canvas { 13 | uint256 totalContributions; 14 | uint256 totalRaised; 15 | mapping(address => uint256) contributions; 16 | mapping(uint256 => uint256) brushUsed; 17 | } 18 | 19 | mapping(uint256 => Canvas) public canvases; 20 | uint256 public startedAt; 21 | 22 | uint256 public openEditionPrice = 0.0026 ether; 23 | uint256 public ownerFeePartsPerMillion = 100_000; // 10% fee 24 | uint256 public ownerEarned; 25 | 26 | event Started(uint256 timestamp); 27 | event Painted(uint256 indexed day, uint256 tokenId, address author, bytes pixels); 28 | 29 | event ArtistsEarned(uint256 indexed day, uint256 amount); 30 | event ArtistWithdraw(uint256 indexed day, address author, uint256 amount); 31 | 32 | event OpenEditionPriceUpdated(uint256 price); 33 | event OwnerFeeUpdated(uint256 fee); 34 | event OwnerWithdrew(uint256 amount, address to); 35 | 36 | constructor(IBasePaintBrush _brushes, uint256 _epochDuration) { 37 | brushes = _brushes; 38 | epochDuration = _epochDuration; 39 | } 40 | 41 | function mint(uint256 day, uint256 count) public payable { 42 | require(startedAt > 0, "Not started"); 43 | require(day + 1 == today(), "Invalid day"); 44 | require(msg.value >= openEditionPrice * count, "Invalid price"); 45 | require(canvases[day].totalContributions > 0, "Empty canvas"); 46 | 47 | _mint(msg.sender, day, count, ""); 48 | 49 | uint256 fee = msg.value * ownerFeePartsPerMillion / 1_000_000; 50 | ownerEarned += fee; 51 | canvases[day].totalRaised += msg.value - fee; 52 | emit ArtistsEarned(day, msg.value - fee); 53 | } 54 | 55 | function paint(uint256 day, uint256 tokenId, bytes calldata pixels) public { 56 | require(startedAt > 0, "Not started"); 57 | require(day == today(), "Invalid day"); 58 | require(brushes.ownerOf(tokenId) == msg.sender, "You don't own this brush"); 59 | require(pixels.length % 3 == 0, "Invalid pixel data"); 60 | require(pixels.length > 0, "Invalid pixel data"); 61 | 62 | uint256 painted = pixels.length / 3; 63 | 64 | Canvas storage canvas = canvases[day]; 65 | canvas.contributions[msg.sender] += painted; 66 | canvas.brushUsed[tokenId] += painted; 67 | canvas.totalContributions += painted; 68 | 69 | require(canvas.brushUsed[tokenId] <= brushes.strengths(tokenId), "Brush used too much"); 70 | emit Painted(day, tokenId, msg.sender, pixels); 71 | } 72 | 73 | function contribution(uint256 day, address author) public view returns (uint256) { 74 | return canvases[day].contributions[author]; 75 | } 76 | 77 | function brushUsed(uint256 day, uint256 tokenId) public view returns (uint256) { 78 | return canvases[day].brushUsed[tokenId]; 79 | } 80 | 81 | function today() public view returns (uint256) { 82 | // Starts from day 1 83 | return ((block.timestamp - startedAt) / epochDuration) + 1; 84 | } 85 | 86 | function authorWithdraw(uint256[] calldata indexes) public { 87 | uint256 maxDay = today() - 1; 88 | for (uint256 i = 0; i < indexes.length; i++) { 89 | uint256 day = indexes[i]; 90 | require(day < maxDay, "Invalid day"); 91 | 92 | Canvas storage canvas = canvases[day]; 93 | require(canvas.totalRaised > 0, "No funds to withdraw"); 94 | require(canvas.totalContributions > 0, "Empty canvas"); 95 | require(canvas.contributions[msg.sender] > 0, "No contributions"); 96 | 97 | uint256 amount = canvas.totalRaised * canvas.contributions[msg.sender] / canvas.totalContributions; 98 | canvas.totalRaised -= amount; 99 | canvas.totalContributions -= canvas.contributions[msg.sender]; 100 | canvas.contributions[msg.sender] = 0; 101 | 102 | (bool success,) = msg.sender.call{value: amount}(""); 103 | require(success, "Transfer failed"); 104 | emit ArtistWithdraw(day, msg.sender, amount); 105 | } 106 | } 107 | 108 | function start() public onlyOwner { 109 | require(startedAt == 0, "Already started"); 110 | 111 | startedAt = block.timestamp; 112 | emit Started(startedAt); 113 | } 114 | 115 | function setURI(string calldata newuri) public onlyOwner { 116 | _setURI(newuri); 117 | } 118 | 119 | function setOwnerFee(uint256 newFee) public onlyOwner { 120 | require(newFee < 1_000_000, "Invalid fee"); 121 | ownerFeePartsPerMillion = newFee; 122 | emit OwnerFeeUpdated(newFee); 123 | } 124 | 125 | function setOpenEditionPrice(uint256 newPrice) public onlyOwner { 126 | openEditionPrice = newPrice; 127 | emit OpenEditionPriceUpdated(newPrice); 128 | } 129 | 130 | function withdraw(address to) public onlyOwner { 131 | uint256 amount = ownerEarned; 132 | ownerEarned = 0; 133 | 134 | (bool success,) = to.call{value: amount}(""); 135 | require(success, "Transfer failed"); 136 | emit OwnerWithdrew(amount, to); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/BasePaintAnimation.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import {ERC1155} from "openzeppelin-contracts/contracts/token/ERC1155/ERC1155.sol"; 5 | import {IERC1155Receiver} from "openzeppelin-contracts/contracts/token/ERC1155/IERC1155Receiver.sol"; 6 | import {Strings} from "openzeppelin-contracts/contracts/utils/Strings.sol"; 7 | import {Ownable} from "openzeppelin-contracts/contracts/access/Ownable.sol"; 8 | 9 | contract BasePaintAnimation is 10 | ERC1155("https://basepaint.xyz/api/animation/{id}"), 11 | Ownable(msg.sender), 12 | IERC1155Receiver 13 | { 14 | error NotSupported(); 15 | error WrongAmount(); 16 | error WrongCollection(); 17 | 18 | address internal immutable _basepaint = 0xBa5e05cb26b78eDa3A2f8e3b3814726305dcAc83; 19 | 20 | function setURI(string calldata newuri) public onlyOwner { 21 | _setURI(newuri); 22 | } 23 | 24 | function onERC1155Received(address, /*operator*/ address from, uint256 id, uint256 value, bytes calldata /*data*/ ) 25 | public 26 | returns (bytes4) 27 | { 28 | if (value == 0) revert WrongAmount(); 29 | if (value % 2 != 0) revert WrongAmount(); 30 | if (msg.sender != _basepaint) revert WrongCollection(); 31 | 32 | // Burn the artwork NFTs 33 | ERC1155(_basepaint).safeTransferFrom(address(this), 0x000000000000000000000000000000000000dEaD, id, value, ""); 34 | 35 | // Mint animation NFT 36 | _mint(from, id, value / 2, ""); 37 | 38 | return this.onERC1155Received.selector; 39 | } 40 | 41 | function onERC1155BatchReceived( 42 | address operator, 43 | address from, 44 | uint256[] calldata ids, 45 | uint256[] calldata values, 46 | bytes calldata data 47 | ) external returns (bytes4) { 48 | for (uint256 i = 0; i < ids.length; i++) { 49 | onERC1155Received(operator, from, ids[i], values[i], data); 50 | } 51 | return this.onERC1155BatchReceived.selector; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/BasePaintBrush.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import {ERC721} from "openzeppelin-contracts/contracts/token/ERC721/ERC721.sol"; 5 | import {IERC721} from "openzeppelin-contracts/contracts/token/ERC721/IERC721.sol"; 6 | import {Ownable} from "openzeppelin-contracts/contracts/access/Ownable.sol"; 7 | import {EIP712} from "openzeppelin-contracts/contracts/utils/cryptography/EIP712.sol"; 8 | import {SignatureChecker} from "openzeppelin-contracts/contracts/utils/cryptography/SignatureChecker.sol"; 9 | 10 | interface IBasePaintBrush is IERC721 { 11 | function strengths(uint256 tokenId) external view returns (uint256); 12 | } 13 | 14 | contract BasePaintBrush is 15 | ERC721("BasePaint Brush", "BPB"), 16 | EIP712("BasePaint Brush", "1"), 17 | IBasePaintBrush, 18 | Ownable(msg.sender) 19 | { 20 | uint256 public totalSupply; 21 | mapping(uint256 => uint256) public strengths; 22 | 23 | address private signer; 24 | mapping(uint256 => bool) private nonces; 25 | string private baseURI = "https://basepaint.xyz/api/brush/"; 26 | 27 | constructor(address newSigner) { 28 | signer = newSigner; 29 | } 30 | 31 | function mint(uint256 strength, uint256 nonce, bytes calldata signature) public payable { 32 | require(!nonces[nonce], "Nonce already used"); 33 | nonces[nonce] = true; 34 | 35 | bytes32 structHash = keccak256( 36 | abi.encode( 37 | keccak256("Mint(address to,uint256 strength,uint256 price,uint256 nonce)"), 38 | msg.sender, 39 | strength, 40 | msg.value, 41 | nonce 42 | ) 43 | ); 44 | 45 | bytes32 digest = _hashTypedDataV4(structHash); 46 | require(SignatureChecker.isValidSignatureNow(signer, digest, signature), "Invalid signature"); 47 | 48 | totalSupply++; 49 | _safeMint(msg.sender, totalSupply); 50 | strengths[totalSupply] = strength; 51 | } 52 | 53 | function upgrade(uint256 tokenId, uint256 strength, uint256 nonce, bytes calldata signature) public payable { 54 | require(tokenId > 0 && tokenId <= totalSupply, "Invalid tokenId"); 55 | require(!nonces[nonce], "Nonce already used"); 56 | nonces[nonce] = true; 57 | 58 | bytes32 structHash = keccak256( 59 | abi.encode( 60 | keccak256("Upgrade(uint256 tokenId,uint256 strength,uint256 price,uint256 nonce)"), 61 | tokenId, 62 | strength, 63 | msg.value, 64 | nonce 65 | ) 66 | ); 67 | 68 | bytes32 digest = _hashTypedDataV4(structHash); 69 | require(SignatureChecker.isValidSignatureNow(signer, digest, signature), "Invalid signature"); 70 | 71 | strengths[tokenId] = strength; 72 | } 73 | 74 | function _baseURI() internal view override returns (string memory) { 75 | return baseURI; 76 | } 77 | 78 | function setSigner(address newSigner) public onlyOwner { 79 | signer = newSigner; 80 | } 81 | 82 | function setBaseURI(string calldata newBaseURI) public onlyOwner { 83 | baseURI = newBaseURI; 84 | } 85 | 86 | function setStrength(uint256 tokenId, uint256 strength) public onlyOwner { 87 | strengths[tokenId] = strength; 88 | } 89 | 90 | function withdraw() public onlyOwner { 91 | (bool success,) = owner().call{value: address(this).balance}(new bytes(0)); 92 | require(success, "Transfer failed"); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/BasePaintBrushEvents.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | interface IBasePaintBrush { 5 | function upgrade(uint256 tokenId, uint256 strength, uint256 nonce, bytes calldata signature) external payable; 6 | } 7 | 8 | contract BasePaintBrushEvents { 9 | IBasePaintBrush public immutable basePaintBrush; 10 | 11 | event Deployed(); 12 | event StrengthChanged(uint256 indexed tokenId, uint256 strength); 13 | 14 | constructor(IBasePaintBrush _basePaintBrush) { 15 | basePaintBrush = _basePaintBrush; 16 | emit Deployed(); 17 | } 18 | 19 | function upgrade(uint256 tokenId, uint256 strength, uint256 nonce, bytes calldata signature) external payable { 20 | basePaintBrush.upgrade{value: msg.value}(tokenId, strength, nonce, signature); 21 | emit StrengthChanged(tokenId, strength); 22 | } 23 | 24 | function upgradeMulti( 25 | uint256[] calldata tokenIds, 26 | uint256[] calldata strengths, 27 | uint256[] calldata nonces, 28 | bytes[] calldata signatures 29 | ) external { 30 | require(tokenIds.length == strengths.length, "Invalid input"); 31 | require(tokenIds.length == nonces.length, "Invalid input"); 32 | require(tokenIds.length == signatures.length, "Invalid input"); 33 | 34 | for (uint256 i = 0; i < tokenIds.length; i++) { 35 | basePaintBrush.upgrade(tokenIds[i], strengths[i], nonces[i], signatures[i]); 36 | emit StrengthChanged(tokenIds[i], strengths[i]); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/BasePaintCollector.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import {IERC1155} from "openzeppelin-contracts/contracts/token/ERC1155/IERC1155.sol"; 5 | import {Ownable} from "openzeppelin-contracts/contracts/access/Ownable.sol"; 6 | 7 | interface IBasePaint is IERC1155 { 8 | function openEditionPrice() external view returns (uint256); 9 | function mint(uint256 day, uint256 count) external payable; 10 | function today() external view returns (uint256); 11 | } 12 | 13 | contract BasePaintCollector { 14 | IBasePaint public basepaint; 15 | 16 | struct Deposit { 17 | uint96 amount; 18 | uint16 lastDayMinted; 19 | uint16 mintPerDay; 20 | } 21 | 22 | mapping(address => Deposit) public deposists; 23 | 24 | constructor(IBasePaint _basepaint) { 25 | basepaint = _basepaint; 26 | } 27 | 28 | function deposit(uint16 mintPerDay) public payable { 29 | deposists[msg.sender].amount += uint96(msg.value); 30 | deposists[msg.sender].mintPerDay += mintPerDay; 31 | } 32 | 33 | function withdraw() public { 34 | uint256 amount = deposists[msg.sender].amount; 35 | require(amount > 0, "No deposit"); 36 | deposists[msg.sender].amount = 0; 37 | 38 | (bool success,) = msg.sender.call{value: amount}(""); 39 | require(success, "Transfer failed"); 40 | } 41 | 42 | function mint(address to) public { 43 | uint256 price = basepaint.openEditionPrice(); 44 | uint256 today = basepaint.today(); 45 | Deposit storage d = deposists[msg.sender]; 46 | 47 | require(d.lastDayMinted < today, "Already minted today"); 48 | d.lastDayMinted = uint16(today); 49 | 50 | uint256 maxCount = d.amount / price; 51 | uint256 count = d.mintPerDay > maxCount ? maxCount : d.mintPerDay; 52 | 53 | require(count > 0, "Out of funds"); 54 | d.amount -= uint96(count * price); 55 | 56 | basepaint.mint{value: price * count}(today, count); 57 | basepaint.safeTransferFrom(address(this), to, today, count, ""); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/BasePaintLoans.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | interface IERC721 { 5 | function ownerOf(uint256 _tokenId) external view returns (address); 6 | function transferFrom(address _from, address _to, uint256 _tokenId) external payable; 7 | } 8 | 9 | interface IWETH { 10 | function deposit() external payable; 11 | function transfer(address dst, uint256 wad) external returns (bool); 12 | } 13 | 14 | interface IBasePaint { 15 | function paint(uint256 day, uint256 tokenId, bytes calldata pixels) external; 16 | function authorWithdraw(uint256[] calldata indexes) external; 17 | } 18 | 19 | interface IDelegateRegistry { 20 | function checkDelegateForERC721(address to, address from, address contract_, uint256 tokenId, bytes32 rights) 21 | external 22 | view 23 | returns (bool); 24 | } 25 | 26 | contract BasePaintLoans { 27 | IWETH internal immutable _weth = IWETH(0x4200000000000000000000000000000000000006); 28 | IERC721 internal immutable _brush = IERC721(0xD68fe5b53e7E1AbeB5A4d0A6660667791f39263a); 29 | IBasePaint internal immutable _basepaint = IBasePaint(0xBa5e05cb26b78eDa3A2f8e3b3814726305dcAc83); 30 | IDelegateRegistry internal immutable _delegate = IDelegateRegistry(0x00000000000000447e69651d841bD8D104Bed493); 31 | 32 | uint256 internal _withdrawingDay; 33 | 34 | struct Contribution { 35 | uint256 totalPoints; 36 | mapping(address => uint256) points; 37 | address[] wallets; 38 | } 39 | 40 | mapping(uint256 => Contribution) public contributions; 41 | mapping(address => uint256) public ownerFeePartsPerMillion; 42 | 43 | event OwnerFeeUpdated(address owner, uint256 fee); 44 | 45 | function paint(uint256 day, uint256 tokenId, bytes calldata pixels) public { 46 | // Make sure the user delegated the brush to the artist 47 | address tokenOwner = _brush.ownerOf(tokenId); 48 | require(_delegate.checkDelegateForERC721(msg.sender, tokenOwner, address(_brush), tokenId, ""), "Not delegated"); 49 | 50 | // Borrow the brush 51 | _brush.transferFrom(tokenOwner, address(this), tokenId); 52 | 53 | // Paint 54 | _basepaint.paint(day, tokenId, pixels); 55 | 56 | uint256 points = 1_000_000 * pixels.length / 3; 57 | uint256 ownerPoints = points * ownerFeePartsPerMillion[tokenOwner] / 1_000_000; 58 | uint256 artistPoints = points - ownerPoints; 59 | 60 | _addPoionts(day, tokenOwner, ownerPoints); 61 | _addPoionts(day, msg.sender, artistPoints); 62 | 63 | // Give it back 64 | _brush.transferFrom(address(this), tokenOwner, tokenId); 65 | } 66 | 67 | function _addPoionts(uint256 day, address author, uint256 points) internal { 68 | if (points == 0) { 69 | return; 70 | } 71 | 72 | Contribution storage contribution = contributions[day]; 73 | if (contribution.points[author] == 0) { 74 | contribution.wallets.push(author); 75 | } 76 | contribution.points[author] += points; 77 | contribution.totalPoints += points; 78 | } 79 | 80 | function setOwnerFee(uint256 newFee) public { 81 | require(newFee <= 1_000_000, "Invalid fee"); 82 | 83 | ownerFeePartsPerMillion[msg.sender] = newFee; 84 | emit OwnerFeeUpdated(msg.sender, newFee); 85 | } 86 | 87 | function withdraw(uint256 day) public { 88 | require(_withdrawingDay == 0, "Already withdrawing"); 89 | 90 | _withdrawingDay = day; 91 | 92 | uint256[] memory indexes = new uint256[](1); 93 | indexes[0] = day; 94 | 95 | _basepaint.authorWithdraw(indexes); 96 | 97 | _withdrawingDay = 0; 98 | } 99 | 100 | receive() external payable { 101 | require(_withdrawingDay > 0, "Invalid day"); 102 | 103 | Contribution storage contribution = contributions[_withdrawingDay]; 104 | for (uint256 i = 0; i < contribution.wallets.length; i++) { 105 | address wallet = contribution.wallets[i]; 106 | uint256 amount = msg.value * contribution.points[wallet] / contribution.totalPoints; 107 | 108 | (bool success,) = wallet.call{value: amount, gas: 40_000}(""); 109 | if (!success) { 110 | _weth.deposit{value: amount}(); 111 | require(_weth.transfer(wallet, amount), "WETH transfer failed"); 112 | } 113 | } 114 | 115 | delete contributions[_withdrawingDay]; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/BasePaintMetadataRegistry.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; 5 | import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; 6 | import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; 7 | 8 | contract BasePaintMetadataRegistry is Initializable, UUPSUpgradeable, OwnableUpgradeable { 9 | struct Metadata { 10 | string name; 11 | uint24[] palette; 12 | uint96 size; 13 | address proposer; 14 | } 15 | 16 | mapping(uint256 => Metadata) private registry; 17 | address public editor; 18 | 19 | event MetadataUpdated(uint256 indexed id, string name, uint24[] palette, uint96 size, address proposer); 20 | event EditorUpdated(address newEditor); 21 | 22 | /// @custom:oz-upgrades-unsafe-allow constructor 23 | constructor() { 24 | _disableInitializers(); 25 | } 26 | 27 | function initialize(address initialOwner, address initialEditor) public initializer { 28 | __Ownable_init(initialOwner); 29 | __UUPSUpgradeable_init(); 30 | editor = initialEditor; 31 | emit EditorUpdated(initialEditor); 32 | } 33 | 34 | modifier onlyEditor() { 35 | require(msg.sender == editor, "not the editor"); 36 | _; 37 | } 38 | 39 | function setEditor(address newEditor) public onlyOwner { 40 | editor = newEditor; 41 | emit EditorUpdated(newEditor); 42 | } 43 | 44 | function setMetadata(uint256 id, string memory name, uint24[] memory palette, uint96 size, address proposer) 45 | public 46 | onlyEditor 47 | { 48 | registry[id] = Metadata(name, palette, size, proposer); 49 | emit MetadataUpdated(id, name, palette, size, proposer); 50 | } 51 | 52 | function batchSetMetadata( 53 | uint256[] memory ids, 54 | string[] memory names, 55 | uint24[][] memory palettes, 56 | uint96[] memory sizes, 57 | address[] memory proposers 58 | ) public onlyEditor { 59 | require( 60 | ids.length == names.length && ids.length == palettes.length && ids.length == sizes.length, 61 | "arrays must have the same length" 62 | ); 63 | 64 | for (uint256 i = 0; i < ids.length; i++) { 65 | registry[ids[i]] = Metadata(names[i], palettes[i], sizes[i], proposers[i]); 66 | emit MetadataUpdated(ids[i], names[i], palettes[i], sizes[i], proposers[i]); 67 | } 68 | } 69 | 70 | function getMetadata(uint256 id) public view returns (Metadata memory) { 71 | return registry[id]; 72 | } 73 | 74 | function getName(uint256 id) public view returns (string memory) { 75 | return registry[id].name; 76 | } 77 | 78 | function getPalette(uint256 id) public view returns (uint24[] memory) { 79 | return registry[id].palette; 80 | } 81 | 82 | function getCanvasSize(uint256 id) public view returns (uint96) { 83 | return registry[id].size; 84 | } 85 | 86 | function getProposer(uint256 id) public view returns (address) { 87 | return registry[id].proposer; 88 | } 89 | 90 | function _authorizeUpgrade(address newImplementation) internal override onlyOwner {} 91 | } 92 | -------------------------------------------------------------------------------- /src/BasePaintRewards.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | // ___ ___ _ __ ___ __ 4 | // / _ )___ ____ ___ / _ \___ _(_)__ / /_ / _ \___ _ _____ ________/ /__ 5 | // / _ / _ `(_- uint256 bips) public rewardRate; // bips, 1 = 0.1% 23 | uint256 public defaultRewardRate = 10; // 1.0% 24 | 25 | error NotEnoughContractFunds(); 26 | error NoRewards(); 27 | error TransferFailed(); 28 | error InvalidRate(); 29 | 30 | event ToppedUp(uint256 amount); 31 | 32 | constructor(IBasePaint _basepaint, address _owner) Ownable(_owner) { 33 | basepaint = _basepaint; 34 | } 35 | 36 | function mintLatest(address sendMintsTo, uint256 count, address sendRewardsTo) public payable { 37 | uint256 tokenIdOnSale = basepaint.today() - 1; 38 | mint(tokenIdOnSale, sendMintsTo, count, sendRewardsTo); 39 | } 40 | 41 | function mint(uint256 tokenId, address sendMintsTo, uint256 count, address sendRewardsTo) public payable { 42 | basepaint.mint{value: msg.value}(tokenId, count); 43 | basepaint.safeTransferFrom(address(this), sendMintsTo, tokenId, count, ""); 44 | 45 | if (sendRewardsTo == address(0)) { 46 | return; 47 | } 48 | 49 | uint256 rate = rewardRate[sendRewardsTo]; 50 | if (rate == 0) { 51 | rate = defaultRewardRate; 52 | } 53 | 54 | uint256 reward = msg.value * rate / 1_000; 55 | _mint(sendRewardsTo, reward); 56 | } 57 | 58 | function cashOut(address account) public { 59 | uint256 available = address(this).balance; 60 | if (available == 0) { 61 | revert NotEnoughContractFunds(); 62 | } 63 | 64 | uint256 balance = balanceOf(account); 65 | if (balance == 0) { 66 | revert NoRewards(); 67 | } 68 | 69 | uint256 withdrawable = available < balance ? available : balance; 70 | 71 | _burn(account, withdrawable); 72 | (bool success,) = account.call{value: withdrawable}(""); 73 | 74 | if (!success) { 75 | revert TransferFailed(); 76 | } 77 | } 78 | 79 | function cashOutBatched(address[] calldata accounts) external { 80 | for (uint256 i = 0; i < accounts.length; i++) { 81 | cashOut(accounts[i]); 82 | } 83 | } 84 | 85 | function setRewardRate(address referrer, uint256 bips) external onlyOwner { 86 | if (bips > 1_000) { 87 | revert InvalidRate(); 88 | } 89 | 90 | if (referrer == address(0)) { 91 | defaultRewardRate = bips; 92 | } else { 93 | rewardRate[referrer] = bips; 94 | } 95 | } 96 | 97 | function withdraw(uint256 value) external onlyOwner { 98 | (bool success,) = msg.sender.call{value: value}(""); 99 | if (!success) { 100 | revert TransferFailed(); 101 | } 102 | } 103 | 104 | receive() external payable { 105 | emit ToppedUp(msg.value); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/BasePaintSubscription.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | // ____ ____ _ _ 4 | // | __ ) __ _ ___ ___ | _ \ __ _(_)_ __ | |_ 5 | // | _ \ / _` / __|/ _ \| |_) / _` | | '_ \| __| 6 | // | |_) | (_| \__ \ __/| __/ (_| | | | | | |_ 7 | // |____/ \__,_|___/\___||_| \__,_|_|_| |_|\__| 8 | // ____ _ _ _ _ 9 | // / ___| _ _| |__ ___ ___ _ __(_)_ __ | |_(_) ___ _ __ 10 | // \___ \| | | | '_ \/ __|/ __| '__| | '_ \| __| |/ _ \| '_ \ 11 | // ___) | |_| | |_) \__ \ (__| | | | |_) | |_| | (_) | | | | 12 | // |____/ \__,_|_.__/|___/\___|_| |_| .__/ \__|_|\___/|_| |_| 13 | // |_| 14 | 15 | pragma solidity ^0.8.17; 16 | 17 | import {OwnableUpgradeable} from "openzeppelin-contracts-upgradeable/contracts/access/OwnableUpgradeable.sol"; 18 | import {IERC1155} from "openzeppelin-contracts/contracts/token/ERC1155/IERC1155.sol"; 19 | import {ERC1155Upgradeable} from "openzeppelin-contracts-upgradeable/contracts/token/ERC1155/ERC1155Upgradeable.sol"; 20 | import {Initializable} from "openzeppelin-contracts-upgradeable/contracts/proxy/utils/Initializable.sol"; 21 | import {UUPSUpgradeable} from "openzeppelin-contracts-upgradeable/contracts/proxy/utils/UUPSUpgradeable.sol"; 22 | 23 | interface IBasePaint is IERC1155 { 24 | function openEditionPrice() external view returns (uint256); 25 | function mint(uint256 day, uint256 count) external payable; 26 | function today() external view returns (uint256); 27 | } 28 | 29 | contract BasePaintSubscription is Initializable, OwnableUpgradeable, ERC1155Upgradeable, UUPSUpgradeable { 30 | IBasePaint public basepaint; 31 | uint256 immutable discountBasisPoints = 500; // 5% 32 | 33 | error WrongEthAmount(); 34 | error InvalidSubscribedDay(); 35 | error DayCountMismatch(); 36 | 37 | function initialize(address _basepaint, address _owner) public initializer { 38 | __Ownable_init(_owner); 39 | __ERC1155_init("https://basepaint.xyz/api/subscription/{id}"); 40 | __UUPSUpgradeable_init(); 41 | 42 | basepaint = IBasePaint(_basepaint); 43 | } 44 | 45 | function subscribe(address _mintToAddress, uint256[] calldata _days, uint256[] calldata _counts) external payable { 46 | if(_days.length != _counts.length) revert DayCountMismatch(); 47 | 48 | uint256 mintingToday = basepaint.today() - 1; 49 | uint256 fullPrice = basepaint.openEditionPrice(); 50 | uint256 discountedPrice = fullPrice * (10000 - discountBasisPoints) / 10000; 51 | uint256 totalCount = 0; 52 | bool isMintingToday = false; 53 | 54 | for (uint256 i = 0; i < _days.length; i++) { 55 | totalCount += _counts[i]; 56 | if (_days[i] < mintingToday) { 57 | revert InvalidSubscribedDay(); 58 | } 59 | if (_days[i] == mintingToday) { 60 | isMintingToday = true; 61 | } 62 | } 63 | 64 | if (msg.value < totalCount * discountedPrice) revert WrongEthAmount(); 65 | 66 | _mintBatch(_mintToAddress, _days, _counts, ""); 67 | 68 | if (isMintingToday) { 69 | address[] memory _addresses = new address[](1); 70 | _addresses[0] = _mintToAddress; 71 | mintBasePaints(_addresses); 72 | } 73 | } 74 | 75 | function mintBasePaints(address[] memory _addresses) public { 76 | uint256 mintingToday = basepaint.today() - 1; 77 | uint256 mintCost = basepaint.openEditionPrice(); 78 | 79 | for (uint256 i = 0; i < _addresses.length; i++) { 80 | uint256 tokenBalance = balanceOf(_addresses[i], mintingToday); 81 | if (tokenBalance > 0) { 82 | _burn(_addresses[i], mintingToday, tokenBalance); 83 | basepaint.mint{value: mintCost * tokenBalance}(mintingToday, tokenBalance); 84 | basepaint.safeTransferFrom(address(this), _addresses[i], mintingToday, tokenBalance, ""); 85 | } 86 | } 87 | } 88 | 89 | function _authorizeUpgrade(address newImplementation) internal override onlyOwner {} 90 | 91 | function onERC1155Received(address, address, uint256, uint256, bytes memory) public pure returns (bytes4) { 92 | return this.onERC1155Received.selector; 93 | } 94 | 95 | function onERC1155BatchReceived(address, address, uint256[] memory, uint256[] memory, bytes memory) 96 | public 97 | pure 98 | returns (bytes4) 99 | { 100 | return this.onERC1155BatchReceived.selector; 101 | } 102 | 103 | receive() external payable {} 104 | } 105 | -------------------------------------------------------------------------------- /src/BasePaintWIP.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import {ERC721} from "openzeppelin-contracts/contracts/token/ERC721/ERC721.sol"; 5 | import {EIP712} from "openzeppelin-contracts/contracts/utils/cryptography/EIP712.sol"; 6 | import {Strings} from "openzeppelin-contracts/contracts/utils/Strings.sol"; 7 | import {Ownable} from "openzeppelin-contracts/contracts/access/Ownable.sol"; 8 | import {SignatureChecker} from "openzeppelin-contracts/contracts/utils/cryptography/SignatureChecker.sol"; 9 | 10 | contract BasePaintWIP is ERC721("BasePaint WIP", "BPWIP"), EIP712("BasePaint WIP", "1"), Ownable(msg.sender) { 11 | address private _signer; 12 | string public baseURI = "https://basepaint.xyz/api/wip/"; 13 | 14 | constructor(address signer) { 15 | _signer = signer; 16 | } 17 | 18 | function mint(bytes32 txHash, bytes calldata signature) public { 19 | bytes32 structHash = keccak256(abi.encode(keccak256("Mint(address to,bytes32 txHash)"), msg.sender, txHash)); 20 | 21 | bytes32 digest = _hashTypedDataV4(structHash); 22 | require(SignatureChecker.isValidSignatureNow(_signer, digest, signature), "Invalid signature"); 23 | 24 | _safeMint(msg.sender, uint256(txHash)); 25 | } 26 | 27 | function tokenURI(uint256 tokenId) public view virtual override returns (string memory) { 28 | _requireOwned(tokenId); 29 | 30 | return string.concat(baseURI, Strings.toHexString(tokenId)); 31 | } 32 | 33 | function setSigner(address signer) public onlyOwner { 34 | _signer = signer; 35 | } 36 | 37 | function setBaseURI(string calldata newBaseURI) public onlyOwner { 38 | baseURI = newBaseURI; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /test/BasePaint.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import {Test, console2} from "forge-std/Test.sol"; 5 | import {IBasePaintBrush} from "../src/BasePaintBrush.sol"; 6 | import {BasePaint} from "../src/BasePaint.sol"; 7 | import {ERC721} from "openzeppelin-contracts/contracts/token/ERC721/ERC721.sol"; 8 | 9 | contract BasePaintTest is Test { 10 | address public alice = address(0xA); 11 | address public bob = address(0xB); 12 | address public safe = address(0x7); 13 | 14 | FakeBrush public brush; 15 | BasePaint public paint; 16 | 17 | function setUp() public { 18 | brush = new FakeBrush(); 19 | paint = new BasePaint(brush, 1 days); 20 | 21 | vm.deal(bob, 100 ether); 22 | } 23 | 24 | function testPaint() public { 25 | brush.mint({to: alice, tokenId: 1, strength: 144}); 26 | paint.start(); 27 | 28 | bytes memory pixels = new bytes(12 * 12 * 3); 29 | uint256 offset = 0; 30 | for (uint256 x = 0; x < 12; x++) { 31 | for (uint256 y = 0; y < 12; y++) { 32 | pixels[offset++] = bytes1(uint8(x + 17)); 33 | pixels[offset++] = bytes1(uint8(y + 0)); 34 | pixels[offset++] = bytes1(uint8(1)); 35 | } 36 | } 37 | 38 | vm.prank(alice); 39 | paint.paint({day: 1, tokenId: 1, pixels: pixels}); 40 | 41 | vm.prank(alice); 42 | vm.expectRevert("Brush used too much"); 43 | paint.paint({day: 1, tokenId: 1, pixels: pixels}); 44 | 45 | vm.warp(block.timestamp + 1 days + 1); 46 | vm.prank(bob); 47 | paint.mint{value: 1 ether}({day: 1, count: 1}); 48 | 49 | assertEq(paint.balanceOf(bob, 1), 1); 50 | 51 | vm.warp(block.timestamp + 1 days + 1); 52 | uint256[] memory indexes = new uint256[](1); 53 | indexes[0] = 1; 54 | uint256 oldAliceBalance = alice.balance; 55 | vm.prank(alice); 56 | paint.authorWithdraw(indexes); 57 | assertEq(alice.balance, oldAliceBalance + 0.9 ether); 58 | 59 | vm.expectRevert("No funds to withdraw"); 60 | vm.prank(alice); 61 | paint.authorWithdraw(indexes); 62 | 63 | paint.withdraw(safe); 64 | assertEq(safe.balance, 0.1 ether); 65 | } 66 | } 67 | 68 | contract FakeBrush is ERC721, IBasePaintBrush { 69 | mapping(uint256 => uint256) public strengths; 70 | 71 | constructor() ERC721("FakeBrush", "FB") {} 72 | 73 | function mint(address to, uint256 tokenId, uint256 strength) public { 74 | _mint(to, tokenId); 75 | strengths[tokenId] = strength; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /test/BasePaintAnimation.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import {Test, console2} from "forge-std/Test.sol"; 5 | import {BasePaintAnimation} from "../src/BasePaintAnimation.sol"; 6 | import {BasePaint} from "../src/BasePaint.sol"; 7 | import {ERC1155} from "openzeppelin-contracts/contracts/token/ERC1155/ERC1155.sol"; 8 | 9 | contract BasePaintAnimationTest is Test { 10 | BasePaintAnimation public animation; 11 | BasePaint public basepaint = BasePaint(0xBa5e05cb26b78eDa3A2f8e3b3814726305dcAc83); 12 | address w1nt3r = address(0x1E79b045Dc29eAe9fdc69673c9DCd7C53E5E159D); 13 | 14 | function setUp() public { 15 | string memory MAINNET_RPC_URL = vm.envString("RPC_URL"); 16 | 17 | vm.createSelectFork(MAINNET_RPC_URL, 19090155); 18 | animation = new BasePaintAnimation(); 19 | } 20 | 21 | function testMintByBurn() public { 22 | vm.prank(w1nt3r); 23 | basepaint.safeTransferFrom(w1nt3r, address(animation), 385, 2, ""); 24 | 25 | assertEq(animation.balanceOf(w1nt3r, 385), 1); 26 | assertEq(basepaint.balanceOf(address(animation), 385), 0); 27 | assertEq(basepaint.balanceOf(0x000000000000000000000000000000000000dEaD, 385), 2); 28 | } 29 | 30 | function testMintByBurnBatched() public { 31 | uint256[] memory ids = new uint256[](2); 32 | uint256[] memory values = new uint256[](2); 33 | ids[0] = 385; 34 | ids[1] = 10; 35 | values[0] = 2; 36 | values[1] = 4; 37 | 38 | vm.prank(w1nt3r); 39 | basepaint.safeBatchTransferFrom(w1nt3r, address(animation), ids, values, ""); 40 | 41 | assertEq(animation.balanceOf(w1nt3r, ids[0]), 1); 42 | assertEq(animation.balanceOf(w1nt3r, ids[1]), 2); 43 | } 44 | 45 | function testWrongValue1() public { 46 | vm.prank(w1nt3r); 47 | vm.expectRevert(); 48 | basepaint.safeTransferFrom(w1nt3r, address(animation), 385, 0, ""); 49 | } 50 | 51 | function testWrongValue2() public { 52 | vm.prank(w1nt3r); 53 | vm.expectRevert(); 54 | basepaint.safeTransferFrom(w1nt3r, address(animation), 385, 3, ""); 55 | } 56 | 57 | function testWrongValue3() public { 58 | vm.prank(w1nt3r); 59 | vm.expectRevert(); 60 | basepaint.safeTransferFrom(w1nt3r, address(animation), 385, 100, ""); 61 | } 62 | 63 | function testWrongNFT() public { 64 | FakeNFT fake = new FakeNFT(); 65 | fake.mint(w1nt3r, 385, 2); 66 | 67 | vm.prank(w1nt3r); 68 | vm.expectRevert(BasePaintAnimation.WrongCollection.selector); 69 | fake.safeTransferFrom(w1nt3r, address(animation), 385, 2, ""); 70 | 71 | assertEq(animation.balanceOf(w1nt3r, 385), 0); 72 | } 73 | 74 | function testURI() public { 75 | animation.setURI("ipfs://{id}"); 76 | assertEq(animation.uri(0), "ipfs://{id}"); 77 | } 78 | } 79 | 80 | contract FakeNFT is ERC1155("") { 81 | function mint(address to, uint256 id, uint256 value) external { 82 | _mint(to, id, value, ""); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /test/BasePaintBrush.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import {Test, console2} from "forge-std/Test.sol"; 5 | import {BasePaintBrush} from "../src/BasePaintBrush.sol"; 6 | 7 | contract BasePaintBrushTest is Test { 8 | address alice = address(0xA); 9 | address signer; 10 | uint256 signerPrivateKey = 0x1234; 11 | 12 | BasePaintBrush public nft; 13 | 14 | function setUp() public { 15 | signer = vm.addr(signerPrivateKey); 16 | nft = new BasePaintBrush(signer); 17 | vm.deal(alice, 10 ether); 18 | } 19 | 20 | function testMintNoSignature() public { 21 | vm.prank(alice); 22 | vm.expectRevert("Invalid signature"); 23 | nft.mint(400, 0, new bytes(0)); 24 | } 25 | 26 | function testMint() public { 27 | bytes32 _TYPE_HASH = 28 | keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); 29 | bytes32 hashedName = keccak256(bytes("BasePaint Brush")); 30 | bytes32 hashedVersion = keccak256(bytes("1")); 31 | bytes32 domain = keccak256(abi.encode(_TYPE_HASH, hashedName, hashedVersion, block.chainid, address(nft))); 32 | 33 | bytes32 structHash = keccak256( 34 | abi.encode( 35 | keccak256("Mint(address to,uint256 strength,uint256 price,uint256 nonce)"), alice, 400, 1 ether, 42 36 | ) 37 | ); 38 | bytes32 digest = keccak256(abi.encodePacked(hex"1901", domain, structHash)); 39 | 40 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPrivateKey, digest); 41 | bytes memory signature = abi.encodePacked(r, s, v); 42 | 43 | vm.prank(alice); 44 | nft.mint{value: 1 ether}(400, 42, signature); 45 | 46 | assertEq(nft.totalSupply(), 1); 47 | assertEq(nft.balanceOf(alice), 1); 48 | assertEq(nft.ownerOf(1), alice); 49 | assertEq(nft.tokenURI(1), "https://basepaint.xyz/api/brush/1"); 50 | assertEq(nft.strengths(1), 400); 51 | 52 | structHash = keccak256( 53 | abi.encode( 54 | keccak256("Upgrade(uint256 tokenId,uint256 strength,uint256 price,uint256 nonce)"), 1, 800, 2 ether, 43 55 | ) 56 | ); 57 | digest = keccak256(abi.encodePacked(hex"1901", domain, structHash)); 58 | 59 | (v, r, s) = vm.sign(signerPrivateKey, digest); 60 | signature = abi.encodePacked(r, s, v); 61 | 62 | vm.prank(alice); 63 | nft.upgrade{value: 2 ether}(1, 800, 43, signature); 64 | assertEq(nft.strengths(1), 800); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /test/BasePaintBrushEvents.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import {Test} from "forge-std/Test.sol"; 5 | import {BasePaintBrushEvents, IBasePaintBrush} from "../src/BasePaintBrushEvents.sol"; 6 | import {ERC721} from "openzeppelin-contracts/contracts/token/ERC721/ERC721.sol"; 7 | 8 | contract BasePaintBrushEventsTest is Test { 9 | FakeBrush public brush; 10 | BasePaintBrushEvents public events; 11 | 12 | function setUp() public { 13 | brush = new FakeBrush(); 14 | events = new BasePaintBrushEvents(IBasePaintBrush(address(brush))); 15 | } 16 | 17 | function testUpgrade() public { 18 | events.upgrade(1, 100, 0, ""); 19 | assertEq(brush.strengths(1), 100); 20 | } 21 | 22 | function testPaidUpgrade() public { 23 | events.upgrade{value: 1 ether}(1, 100, 0, ""); 24 | 25 | assertEq(brush.strengths(1), 100); 26 | assertEq(address(brush).balance, 1 ether); 27 | } 28 | 29 | function testupgradeMulti() public { 30 | uint256[] memory tokenIds = new uint256[](2); 31 | uint256[] memory strengths = new uint256[](2); 32 | uint256[] memory nonces = new uint256[](2); 33 | bytes[] memory signatures = new bytes[](2); 34 | 35 | tokenIds[0] = 1; 36 | strengths[0] = 100; 37 | nonces[0] = 0; 38 | signatures[0] = ""; 39 | 40 | tokenIds[1] = 2; 41 | strengths[1] = 200; 42 | nonces[1] = 0; 43 | signatures[1] = ""; 44 | 45 | events.upgradeMulti(tokenIds, strengths, nonces, signatures); 46 | assertEq(brush.strengths(1), 100); 47 | assertEq(brush.strengths(2), 200); 48 | } 49 | } 50 | 51 | contract FakeBrush is IBasePaintBrush { 52 | mapping(uint256 => uint256) public strengths; 53 | 54 | function upgrade(uint256 tokenId, uint256 strength, uint256, bytes calldata) external payable { 55 | strengths[tokenId] = strength; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /test/BasePaintMetadataRegistry.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.13; 3 | 4 | import {Test} from "forge-std/Test.sol"; 5 | import {BasePaintMetadataRegistry} from "../src/BasePaintMetadataRegistry.sol"; 6 | import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; 7 | import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; 8 | import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; 9 | 10 | contract BasePaintMetadataRegistryTest is Test { 11 | ERC1967Proxy proxy; 12 | BasePaintMetadataRegistry public implementation; 13 | BasePaintMetadataRegistry public registry; 14 | address public owner; 15 | address public editor; 16 | address public user; 17 | 18 | event MetadataUpdated(uint256 indexed id, string name, uint24[] palette, uint96 size, address proposer); 19 | event EditorUpdated(address newEditor); 20 | 21 | function setUp() public { 22 | owner = address(this); 23 | editor = address(0x1); 24 | user = address(0x2); 25 | 26 | implementation = new BasePaintMetadataRegistry(); 27 | 28 | bytes memory data = abi.encodeWithSelector(BasePaintMetadataRegistry.initialize.selector, owner, editor); 29 | 30 | proxy = new ERC1967Proxy(address(implementation), data); 31 | registry = BasePaintMetadataRegistry(address(proxy)); 32 | } 33 | 34 | function testInitialization() public { 35 | assertEq(registry.owner(), owner); 36 | assertEq(registry.editor(), editor); 37 | } 38 | 39 | function testSetMetadata() public { 40 | uint256 id = 1; 41 | string memory name = "Test Theme"; 42 | uint24[] memory palette = new uint24[](3); 43 | palette[0] = 0xFF0000; 44 | palette[1] = 0x00FF00; 45 | palette[2] = 0x0000FF; 46 | uint96 size = 100; 47 | address proposer = address(0x3); 48 | 49 | vm.prank(editor); 50 | vm.expectEmit(true, false, false, true); 51 | emit MetadataUpdated(id, name, palette, size, proposer); 52 | registry.setMetadata(id, name, palette, size, proposer); 53 | 54 | BasePaintMetadataRegistry.Metadata memory data = registry.getMetadata(id); 55 | assertEq(data.name, name); 56 | assertEq(data.palette.length, 3); 57 | assertEq(data.palette[0], 0xFF0000); 58 | assertEq(data.palette[1], 0x00FF00); 59 | assertEq(data.palette[2], 0x0000FF); 60 | assertEq(data.size, size); 61 | assertEq(data.proposer, proposer); 62 | } 63 | 64 | function testBatchSetMetadata() public { 65 | uint256[] memory ids = new uint256[](2); 66 | ids[0] = 1; 67 | ids[1] = 2; 68 | 69 | string[] memory names = new string[](2); 70 | names[0] = "Theme 1"; 71 | names[1] = "Theme 2"; 72 | 73 | uint24[][] memory palettes = new uint24[][](2); 74 | palettes[0] = new uint24[](2); 75 | palettes[0][0] = 0xFF0000; 76 | palettes[0][1] = 0x00FF00; 77 | palettes[1] = new uint24[](3); 78 | palettes[1][0] = 0x0000FF; 79 | palettes[1][1] = 0xFFFF00; 80 | palettes[1][2] = 0xFF00FF; 81 | 82 | uint96[] memory sizes = new uint96[](2); 83 | sizes[0] = 100; 84 | sizes[1] = 200; 85 | 86 | address[] memory proposers = new address[](2); 87 | proposers[0] = address(0x3); 88 | proposers[1] = address(0x4); 89 | 90 | vm.prank(editor); 91 | registry.batchSetMetadata(ids, names, palettes, sizes, proposers); 92 | 93 | for (uint256 i = 0; i < ids.length; i++) { 94 | BasePaintMetadataRegistry.Metadata memory data = registry.getMetadata(ids[i]); 95 | assertEq(data.name, names[i]); 96 | assertEq(data.palette.length, palettes[i].length); 97 | for (uint256 j = 0; j < palettes[i].length; j++) { 98 | assertEq(data.palette[j], palettes[i][j]); 99 | } 100 | assertEq(data.size, sizes[i]); 101 | assertEq(data.proposer, proposers[i]); 102 | } 103 | } 104 | 105 | function testGetters() public { 106 | uint256 id = 1; 107 | string memory name = "Test Theme"; 108 | uint24[] memory palette = new uint24[](3); 109 | palette[0] = 0xFF0000; 110 | palette[1] = 0x00FF00; 111 | palette[2] = 0x0000FF; 112 | uint96 size = 100; 113 | address proposer = address(0x3); 114 | 115 | vm.prank(editor); 116 | registry.setMetadata(id, name, palette, size, proposer); 117 | 118 | assertEq(registry.getName(id), name); 119 | 120 | uint24[] memory retrievedPalette = registry.getPalette(id); 121 | assertEq(retrievedPalette.length, palette.length); 122 | for (uint256 i = 0; i < palette.length; i++) { 123 | assertEq(retrievedPalette[i], palette[i]); 124 | } 125 | 126 | assertEq(registry.getCanvasSize(id), size); 127 | assertEq(registry.getProposer(id), proposer); 128 | } 129 | 130 | function testOnlyEditorCanSetMetadata() public { 131 | vm.prank(user); 132 | vm.expectRevert("not the editor"); 133 | registry.setMetadata(1, "Test", new uint24[](0), 0, address(0)); 134 | 135 | vm.prank(owner); 136 | vm.expectRevert("not the editor"); 137 | registry.setMetadata(1, "Test", new uint24[](0), 0, address(0)); 138 | } 139 | 140 | function testOnlyEditorCanBatchSetMetadata() public { 141 | vm.prank(user); 142 | vm.expectRevert("not the editor"); 143 | registry.batchSetMetadata( 144 | new uint256[](1), new string[](1), new uint24[][](1), new uint96[](1), new address[](1) 145 | ); 146 | 147 | vm.prank(owner); 148 | vm.expectRevert("not the editor"); 149 | registry.batchSetMetadata( 150 | new uint256[](1), new string[](1), new uint24[][](1), new uint96[](1), new address[](1) 151 | ); 152 | } 153 | 154 | function testBatchSetMetadataWithMismatchedArrays() public { 155 | uint256[] memory ids = new uint256[](2); 156 | string[] memory names = new string[](1); 157 | uint24[][] memory palettes = new uint24[][](2); 158 | uint96[] memory sizes = new uint96[](2); 159 | address[] memory proposers = new address[](2); 160 | 161 | vm.prank(editor); 162 | vm.expectRevert("arrays must have the same length"); 163 | registry.batchSetMetadata(ids, names, palettes, sizes, proposers); 164 | } 165 | 166 | function testSetEditor() public { 167 | address newEditor = address(0x5); 168 | 169 | vm.prank(owner); 170 | vm.expectEmit(true, false, false, true); 171 | emit EditorUpdated(newEditor); 172 | registry.setEditor(newEditor); 173 | 174 | assertEq(registry.editor(), newEditor); 175 | } 176 | 177 | function testOnlyOwnerCanSetEditor() public { 178 | address newEditor = address(0x5); 179 | 180 | vm.prank(user); 181 | vm.expectRevert(abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, user)); 182 | registry.setEditor(newEditor); 183 | 184 | vm.prank(editor); 185 | vm.expectRevert(abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, editor)); 186 | registry.setEditor(newEditor); 187 | 188 | assertEq(registry.editor(), editor); 189 | } 190 | 191 | function testSetEditorPermissions() public { 192 | address newEditor = address(0x5); 193 | 194 | vm.prank(owner); 195 | registry.setEditor(newEditor); 196 | 197 | uint256 id = 1; 198 | string memory name = "Test Theme"; 199 | uint24[] memory palette = new uint24[](3); 200 | uint96 size = 100; 201 | address proposer = address(0x3); 202 | 203 | vm.prank(editor); 204 | vm.expectRevert("not the editor"); 205 | registry.setMetadata(id, name, palette, size, proposer); 206 | 207 | vm.prank(newEditor); 208 | registry.setMetadata(id, name, palette, size, proposer); 209 | 210 | BasePaintMetadataRegistry.Metadata memory data = registry.getMetadata(id); 211 | assertEq(data.name, name); 212 | } 213 | 214 | function testUpgrade() public { 215 | BasePaintMetadataRegistryV2 newImplementation = new BasePaintMetadataRegistryV2(); 216 | bytes memory data = abi.encodeWithSelector(BasePaintMetadataRegistryV2.upgradeHasWorkedJustFine.selector); 217 | 218 | vm.prank(owner); 219 | registry.upgradeToAndCall(address(newImplementation), data); 220 | assertEq(BasePaintMetadataRegistryV2(address(registry)).upgradeHasWorkedJustFine(), "upgradeHasWorkedJustFine"); 221 | } 222 | 223 | function testUpgradeNotOwner() public { 224 | BasePaintMetadataRegistryV2 newImplementation = new BasePaintMetadataRegistryV2(); 225 | bytes memory data = abi.encodeWithSelector(BasePaintMetadataRegistryV2.upgradeHasWorkedJustFine.selector); 226 | 227 | vm.prank(user); 228 | vm.expectRevert(abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, user)); 229 | registry.upgradeToAndCall(address(newImplementation), data); 230 | 231 | vm.prank(editor); 232 | vm.expectRevert(abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, editor)); 233 | registry.upgradeToAndCall(address(newImplementation), data); 234 | } 235 | 236 | function testUpgradeWithBadCall() public { 237 | BasePaintMetadataRegistryV2 newImplementation = new BasePaintMetadataRegistryV2(); 238 | bytes memory data = abi.encodeWithSelector(BasePaintMetadataRegistry.initialize.selector, owner, editor); 239 | 240 | vm.prank(owner); 241 | vm.expectRevert(abi.encodeWithSelector(Initializable.InvalidInitialization.selector)); 242 | registry.upgradeToAndCall(address(newImplementation), data); 243 | } 244 | } 245 | 246 | contract BasePaintMetadataRegistryV2 is BasePaintMetadataRegistry { 247 | function upgradeHasWorkedJustFine() public pure returns (string memory) { 248 | return "upgradeHasWorkedJustFine"; 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /test/BasePaintRewards.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import {Test, console2} from "forge-std/Test.sol"; 5 | import {IBasePaintBrush} from "../src/BasePaintBrush.sol"; 6 | import {BasePaint} from "../src/BasePaint.sol"; 7 | import {BasePaintRewards, IBasePaint} from "../src/BasePaintRewards.sol"; 8 | import {ERC721} from "openzeppelin-contracts/contracts/token/ERC721/ERC721.sol"; 9 | 10 | contract BasePaintRewardsTest is Test { 11 | address public alice = address(0xA); 12 | address public bob = address(0xB); 13 | address public safe = address(0x7); 14 | 15 | FakeBrush public brush; 16 | BasePaint public paint; 17 | BasePaintRewards public rewards; 18 | 19 | function setUp() public { 20 | brush = new FakeBrush(); 21 | paint = new BasePaint(brush, 1 days); 22 | rewards = new BasePaintRewards(IBasePaint(address(paint)), address(this)); 23 | 24 | vm.deal(bob, 100 ether); 25 | 26 | brush.mint({to: alice, tokenId: 1, strength: 200}); 27 | paint.start(); 28 | 29 | bytes memory pixels = new bytes(12 * 12 * 3); 30 | vm.prank(alice); 31 | paint.paint({day: 1, tokenId: 1, pixels: pixels}); 32 | 33 | vm.warp(block.timestamp + 1 days + 1); 34 | } 35 | 36 | function testMintWithRewards() public { 37 | uint256 price = paint.openEditionPrice(); 38 | rewards.mint{value: price}({tokenId: 1, sendMintsTo: bob, count: 1, sendRewardsTo: safe}); 39 | 40 | assertEq(paint.balanceOf(bob, 1), 1); 41 | assertEq(rewards.balanceOf(safe), price * rewards.defaultRewardRate() / 1_000); 42 | 43 | vm.expectRevert(BasePaintRewards.NotEnoughContractFunds.selector); 44 | rewards.cashOut(safe); 45 | 46 | vm.deal(address(rewards), 1 ether); 47 | 48 | vm.expectRevert(BasePaintRewards.NoRewards.selector); 49 | rewards.cashOut(bob); 50 | 51 | rewards.cashOut(safe); 52 | assertEq(rewards.balanceOf(safe), 0); 53 | assertEq(safe.balance, 0.000026 ether); 54 | } 55 | 56 | function testMintWithCustomRewards() public { 57 | uint256 price = paint.openEditionPrice(); 58 | rewards.setRewardRate(safe, 1_000); // 100% 59 | rewards.mintLatest{value: price}({sendMintsTo: bob, count: 1, sendRewardsTo: safe}); 60 | 61 | vm.deal(address(rewards), 1 ether); 62 | rewards.cashOut(safe); 63 | assertEq(rewards.balanceOf(safe), 0); 64 | assertEq(safe.balance, 0.0026 ether); 65 | } 66 | 67 | function testMintMany() public { 68 | uint256 price = paint.openEditionPrice(); 69 | 70 | for (uint256 i = 0; i < 10; i++) { 71 | rewards.mint{value: price}({ 72 | tokenId: 1, 73 | sendMintsTo: address(uint160(bob) + uint160(i)), 74 | count: 1, 75 | sendRewardsTo: safe 76 | }); 77 | } 78 | } 79 | } 80 | 81 | contract FakeBrush is ERC721, IBasePaintBrush { 82 | mapping(uint256 => uint256) public strengths; 83 | 84 | constructor() ERC721("FakeBrush", "FB") {} 85 | 86 | function mint(address to, uint256 tokenId, uint256 strength) public { 87 | _mint(to, tokenId); 88 | strengths[tokenId] = strength; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /test/BasePaintSubscription.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.17; 3 | 4 | import "forge-std/Test.sol"; 5 | import "../src/BasePaintSubscription.sol"; 6 | import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol"; 7 | import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; 8 | 9 | contract MockBasePaint is ERC1155 { 10 | uint256 private _openEditionPrice = 0.0026 ether; 11 | uint256 private _today = 600; 12 | 13 | constructor() ERC1155("") {} 14 | 15 | function openEditionPrice() external view returns (uint256) { 16 | return _openEditionPrice; 17 | } 18 | 19 | function setOpenEditionPrice(uint256 price) external { 20 | _openEditionPrice = price; 21 | } 22 | 23 | function today() external view returns (uint256) { 24 | return _today; 25 | } 26 | 27 | function setToday(uint256 day) external { 28 | _today = day; 29 | } 30 | 31 | function mint(uint256 day, uint256 count) external payable { 32 | require(msg.value == count * _openEditionPrice, "Incorrect payment"); 33 | _mint(msg.sender, day, count, ""); 34 | } 35 | } 36 | 37 | contract BasePaintSubscriptionV2 is BasePaintSubscription { 38 | uint256 public specialDiscountBasisPoints; 39 | 40 | function setSpecialDiscountBasisPoints(uint256 _specialDiscountBasisPoints) external onlyOwner { 41 | specialDiscountBasisPoints = _specialDiscountBasisPoints; 42 | } 43 | 44 | function getSpecialDiscountedPrice(uint256 price) public view returns (uint256) { 45 | return (price * (10000 - specialDiscountBasisPoints)) / 10000; 46 | } 47 | } 48 | 49 | contract BasePaintSubscriptionTest is Test { 50 | BasePaintSubscription public subscription; 51 | MockBasePaint public mockBasePaint; 52 | address public owner = address(0x1); 53 | address public user1 = address(0x2); 54 | address public user2 = address(0x3); 55 | uint256 public openEditionPrice = 0.0026 ether; 56 | uint256 public constant DISCOUNT_BASIS_POINTS = 500; // 5% 57 | 58 | function setUp() public { 59 | mockBasePaint = new MockBasePaint(); 60 | 61 | vm.startPrank(owner); 62 | BasePaintSubscription impl = new BasePaintSubscription(); 63 | bytes memory initData = 64 | abi.encodeWithSelector(BasePaintSubscription.initialize.selector, address(mockBasePaint), owner); 65 | ERC1967Proxy proxy = new ERC1967Proxy(address(impl), initData); 66 | subscription = BasePaintSubscription(payable(address(proxy))); 67 | vm.stopPrank(); 68 | 69 | vm.deal(user1, 100 ether); 70 | vm.deal(user2, 100 ether); 71 | vm.deal(address(subscription), 100 ether); 72 | } 73 | 74 | function test_Initialize() public { 75 | assertEq(address(subscription.basepaint()), address(mockBasePaint)); 76 | assertEq(subscription.owner(), owner); 77 | } 78 | 79 | function calculateDiscountedPrice(uint256 price, uint256 discountBasisPoints) internal pure returns (uint256) { 80 | return (price * (10000 - discountBasisPoints)) / 10000; 81 | } 82 | 83 | function test_Subscribe() public { 84 | uint256[] memory ids = new uint256[](1); 85 | uint256[] memory counts = new uint256[](1); 86 | 87 | ids[0] = 601; 88 | counts[0] = 2; 89 | 90 | uint256 discountedPrice = calculateDiscountedPrice(openEditionPrice, DISCOUNT_BASIS_POINTS); 91 | uint256 totalCost = 2 * discountedPrice; 92 | 93 | vm.prank(user1); 94 | subscription.subscribe{value: totalCost}(user1, ids, counts); 95 | 96 | assertEq(subscription.balanceOf(user1, 601), 2); 97 | } 98 | 99 | function test_SubscribeForOtherAddress() public { 100 | uint256[] memory ids = new uint256[](1); 101 | uint256[] memory counts = new uint256[](1); 102 | 103 | ids[0] = 601; 104 | counts[0] = 2; 105 | 106 | uint256 discountedPrice = calculateDiscountedPrice(openEditionPrice, DISCOUNT_BASIS_POINTS); 107 | uint256 totalCost = 2 * discountedPrice; 108 | 109 | vm.prank(user1); 110 | subscription.subscribe{value: totalCost}(user2, ids, counts); 111 | 112 | assertEq(subscription.balanceOf(user2, 601), 2); 113 | assertEq(subscription.balanceOf(user1, 601), 0); 114 | } 115 | 116 | function test_RevertWhen_SubscribeWithWrongEthAmount() public { 117 | uint256[] memory ids = new uint256[](1); 118 | uint256[] memory counts = new uint256[](1); 119 | 120 | ids[0] = 601; 121 | counts[0] = 2; 122 | 123 | uint256 incorrectCost = openEditionPrice; 124 | 125 | vm.prank(user1); 126 | vm.expectRevert(BasePaintSubscription.WrongEthAmount.selector); 127 | subscription.subscribe{value: incorrectCost}(user1, ids, counts); 128 | } 129 | 130 | function test_RevertWhen_SubscribeForPastDay() public { 131 | mockBasePaint.setToday(600); 132 | uint256 pastDay = 598; 133 | 134 | uint256[] memory ids = new uint256[](1); 135 | uint256[] memory counts = new uint256[](1); 136 | 137 | ids[0] = pastDay; 138 | counts[0] = 2; 139 | 140 | uint256 discountedPrice = calculateDiscountedPrice(openEditionPrice, DISCOUNT_BASIS_POINTS); 141 | uint256 totalCost = 2 * discountedPrice; 142 | 143 | vm.prank(user1); 144 | vm.expectRevert(BasePaintSubscription.InvalidSubscribedDay.selector); 145 | subscription.subscribe{value: totalCost}(user1, ids, counts); 146 | } 147 | 148 | function test_MintBasePaints() public { 149 | mockBasePaint.setToday(800); 150 | 151 | uint256[] memory ids = new uint256[](1); 152 | uint256[] memory counts = new uint256[](1); 153 | 154 | ids[0] = 800; 155 | counts[0] = 3; 156 | 157 | uint256 discountedPrice = calculateDiscountedPrice(openEditionPrice, DISCOUNT_BASIS_POINTS); 158 | uint256 totalCost = 3 * discountedPrice; 159 | 160 | vm.prank(user1); 161 | subscription.subscribe{value: totalCost}(user1, ids, counts); 162 | 163 | mockBasePaint.setToday(801); 164 | 165 | address[] memory addresses = new address[](1); 166 | addresses[0] = user1; 167 | 168 | vm.prank(user1); 169 | subscription.mintBasePaints(addresses); 170 | 171 | assertEq(subscription.balanceOf(user1, 800), 0); 172 | assertEq(mockBasePaint.balanceOf(user1, 800), 3); 173 | } 174 | 175 | function test_MintWithDifferentUsers() public { 176 | mockBasePaint.setToday(800); 177 | 178 | uint256[] memory ids = new uint256[](1); 179 | uint256[] memory counts = new uint256[](1); 180 | 181 | ids[0] = 800; 182 | counts[0] = 2; 183 | 184 | uint256 discountedPrice = calculateDiscountedPrice(openEditionPrice, DISCOUNT_BASIS_POINTS); 185 | uint256 totalCost1 = 2 * discountedPrice; 186 | 187 | vm.prank(user1); 188 | subscription.subscribe{value: totalCost1}(user1, ids, counts); 189 | 190 | ids[0] = 800; 191 | counts[0] = 3; 192 | 193 | uint256 totalCost2 = 3 * discountedPrice; 194 | 195 | vm.prank(user2); 196 | subscription.subscribe{value: totalCost2}(user2, ids, counts); 197 | 198 | assertEq(subscription.balanceOf(user1, 800), 2); 199 | assertEq(subscription.balanceOf(user2, 800), 3); 200 | 201 | mockBasePaint.setToday(801); 202 | 203 | address[] memory addresses = new address[](2); 204 | addresses[0] = user1; 205 | addresses[1] = user2; 206 | 207 | vm.prank(owner); 208 | subscription.mintBasePaints(addresses); 209 | 210 | assertEq(subscription.balanceOf(user1, 800), 0); 211 | assertEq(subscription.balanceOf(user2, 800), 0); 212 | } 213 | 214 | function test_MintMultipleBasePaints() public { 215 | mockBasePaint.setToday(800); 216 | 217 | uint256[] memory ids = new uint256[](3); 218 | uint256[] memory counts = new uint256[](3); 219 | 220 | ids[0] = 801; 221 | counts[0] = 1; 222 | 223 | ids[1] = 802; 224 | counts[1] = 2; 225 | 226 | ids[2] = 803; 227 | counts[2] = 3; 228 | 229 | uint256 discountedPrice = calculateDiscountedPrice(openEditionPrice, DISCOUNT_BASIS_POINTS); 230 | uint256 totalCost = 1 * discountedPrice + 2 * discountedPrice + 3 * discountedPrice; 231 | 232 | vm.prank(user1); 233 | subscription.subscribe{value: totalCost}(user1, ids, counts); 234 | 235 | assertEq(subscription.balanceOf(user1, 801), 1); 236 | assertEq(subscription.balanceOf(user1, 802), 2); 237 | assertEq(subscription.balanceOf(user1, 803), 3); 238 | } 239 | 240 | function test_UpgradeContract() public { 241 | BasePaintSubscriptionV2 newImplementation = new BasePaintSubscriptionV2(); 242 | 243 | vm.prank(owner); 244 | subscription.upgradeToAndCall(address(newImplementation), ""); 245 | 246 | BasePaintSubscriptionV2 upgradedContract = BasePaintSubscriptionV2(payable(address(subscription))); 247 | 248 | assertEq(upgradedContract.specialDiscountBasisPoints(), 0); 249 | 250 | vm.prank(owner); 251 | upgradedContract.setSpecialDiscountBasisPoints(1500); // 15% 252 | 253 | assertEq(upgradedContract.specialDiscountBasisPoints(), 1500); 254 | 255 | uint256 originalPrice = 10000; 256 | uint256 expectedDiscountedPrice = 8500; // 10000 - 15% 257 | assertEq(upgradedContract.getSpecialDiscountedPrice(originalPrice), expectedDiscountedPrice); 258 | } 259 | 260 | function test_RevertWhen_NotOwnerUpgrade() public { 261 | BasePaintSubscription newImplementation = new BasePaintSubscription(); 262 | 263 | vm.prank(user1); 264 | vm.expectRevert(); 265 | subscription.upgradeToAndCall(address(newImplementation), ""); 266 | } 267 | 268 | function test_ReceiveEther() public { 269 | uint256 initialBalance = address(subscription).balance; 270 | 271 | (bool success,) = address(subscription).call{value: 1 ether}(""); 272 | assertTrue(success); 273 | 274 | assertEq(address(subscription).balance, initialBalance + 1 ether); 275 | } 276 | } 277 | -------------------------------------------------------------------------------- /test/BasePaintWIP.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import {Test, console2} from "forge-std/Test.sol"; 5 | import {BasePaintWIP} from "../src/BasePaintWIP.sol"; 6 | 7 | contract BasePaintWIPTest is Test { 8 | address alice = address(0xA); 9 | address signer; 10 | uint256 signerPrivateKey = 0x1234; 11 | 12 | BasePaintWIP public nft; 13 | 14 | function setUp() public { 15 | signer = vm.addr(signerPrivateKey); 16 | nft = new BasePaintWIP(signer); 17 | vm.deal(alice, 10 ether); 18 | } 19 | 20 | function testMintNoSignature() public { 21 | vm.prank(alice); 22 | vm.expectRevert("Invalid signature"); 23 | nft.mint(0xdfa855998287407ae494c73707512376a2e1debfe05159996af8ec09a89fce87, new bytes(0)); 24 | } 25 | 26 | function testMint() public { 27 | bytes32 typeHash = 28 | keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); 29 | bytes32 hashedName = keccak256(bytes("BasePaint WIP")); 30 | bytes32 hashedVersion = keccak256(bytes("1")); 31 | bytes32 domain = keccak256(abi.encode(typeHash, hashedName, hashedVersion, block.chainid, address(nft))); 32 | 33 | bytes32 structHash = keccak256( 34 | abi.encode( 35 | keccak256("Mint(address to,bytes32 txHash)"), 36 | alice, 37 | 0xdfa855998287407ae494c73707512376a2e1debfe05159996af8ec09a89fce87 38 | ) 39 | ); 40 | bytes32 digest = keccak256(abi.encodePacked(hex"1901", domain, structHash)); 41 | 42 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPrivateKey, digest); 43 | bytes memory signature = abi.encodePacked(r, s, v); 44 | 45 | vm.prank(alice); 46 | nft.mint(0xdfa855998287407ae494c73707512376a2e1debfe05159996af8ec09a89fce87, signature); 47 | 48 | assertEq(nft.balanceOf(alice), 1); 49 | assertEq(nft.ownerOf(0xdfa855998287407ae494c73707512376a2e1debfe05159996af8ec09a89fce87), alice); 50 | assertEq( 51 | nft.tokenURI(0xdfa855998287407ae494c73707512376a2e1debfe05159996af8ec09a89fce87), 52 | "https://basepaint.xyz/api/wip/0xdfa855998287407ae494c73707512376a2e1debfe05159996af8ec09a89fce87" 53 | ); 54 | } 55 | } 56 | --------------------------------------------------------------------------------