├── foundry.toml ├── .gitignore ├── .gitmodules ├── script └── Soulbound.s.sol ├── .github └── workflows │ └── test.yml ├── README.md ├── src └── Soulbound.sol └── test └── Soulbound.t.sol /foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | src = "src" 3 | out = "out" 4 | libs = ["lib"] 5 | 6 | # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options 7 | verbosity = 5 8 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /script/Soulbound.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.19; 3 | 4 | import {Script, console2} from "forge-std/Script.sol"; 5 | import {Soulbound} from "../src/Soulbound.sol"; 6 | 7 | contract SoulboundScript is Script { 8 | Soulbound public soulbound; 9 | 10 | function setUp() public { 11 | soulbound = new Soulbound(); 12 | } 13 | 14 | function run() public { 15 | vm.broadcast(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.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@v3 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SoulboundNFT Smart Contract Documentation 2 | 3 | ## Introduction 4 | 5 | The contract ensures that each wallet can possess only one soulbound main token and allows the transaction of item tokens only between wallets possessing the soulbound main token. 6 | 7 | ## ERC1155 Token Standards 8 | 9 | The contract adheres to the ERC1155 token standard, which is a multi-token standard on the Ethereum blockchain. It enables the creation of both fungible and non-fungible tokens within a single contract. 10 | 11 | ## Functionality 12 | 13 | ### Soulbound Main Token 14 | 15 | - **Soulbound Token ID:** The contract defines a unique Soulbound Token ID (e.g., 1) representing the main character token. 16 | - **Ownership Limitation:** Each wallet can own only one soulbound main token. 17 | - **Ownership Tracking:** The contract keeps track of the owner of the soulbound token using a mapping. 18 | 19 | ### Item Tokens 20 | 21 | - **Minting Item Tokens:** The contract allows the owner to mint item tokens that can only be transacted between wallets owning the soulbound main token. 22 | - **Transferring Item Tokens:** Item tokens can be transferred between wallets, but only if both the sender and receiver own the soulbound main token. 23 | - **Ownership Verification:** A modifier ensures that only the owner of the soulbound token can perform certain functions. 24 | 25 | ### Contract Deployment 26 | 27 | - **Constructor:** The constructor function initializes the contract, mints a soulbound token to the contract creator, and designates the creator as the initial soulbound token owner. 28 | 29 | ### Administrative Functions 30 | 31 | - **Minting Item Tokens:** The contract owner can mint item tokens and distribute them to wallet addresses owning the soulbound token. 32 | - **Soulbound Token Ownership Transfer:** The ownership of the soulbound token can be transferred to another address by the contract owner. 33 | 34 | ## Deployment 35 | 36 | To contract was built using foundry tooling and tested using `forge test` command, the contract was deployed to sepolia using the `forge create` command and can be accessed here https://sepolia.etherscan.io/address/0x3085c8f0e3edb907a6961cd361b0d01b13f522e3 37 | 38 | ## Usage 39 | 40 | 1. **Owning a Soulbound Token:** 41 | - Wallets can check if they own the soulbound token by calling the `ownsSoulboundToken` function. 42 | 43 | 2. **Minting Item Tokens:** 44 | - The contract owner can mint item tokens using the `mintItemTokens` function, providing the recipient's address, item IDs, and corresponding amounts. 45 | 46 | 3. **Transferring Item Tokens:** 47 | - Wallet owners can transfer item tokens to other wallets owning the soulbound token by calling the `transferItemTokens` function, providing the recipient's address, item IDs, and corresponding amounts. 48 | 49 | 4. **Transferring Soulbound Token Ownership:** 50 | - The contract owner can transfer the ownership of the soulbound token to another address using the `transferSoulboundTokenOwnership` function. 51 | -------------------------------------------------------------------------------- /src/Soulbound.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | // Compiler Version 4 | pragma solidity ^0.8.19; 5 | 6 | // Main standard that handles multitokens ERC1155 7 | import "../lib/openzeppelin-contracts/contracts/token/ERC1155/ERC1155.sol"; 8 | 9 | // Main contract for handling ownership and access control 10 | import "../lib/openzeppelin-contracts/contracts/access/Ownable.sol"; 11 | 12 | contract Soulbound is ERC1155, Ownable { 13 | 14 | // Main token representing a character (soulbound) 15 | uint256 public constant SOULBOUND_TOKEN_ID = 1; 16 | 17 | // Mapping to keep track of the owner of the soulbound token 18 | mapping(address => bool) private soulboundTokenOwners; 19 | 20 | // Modifier to check if the caller owns the soulbound token 21 | modifier onlySoulboundTokenOwner() { 22 | require(soulboundTokenOwners[msg.sender], "Not owner of soulbound token"); 23 | _; 24 | } 25 | 26 | constructor() ERC1155("https://ipfs.io/ipfs/bafybeigxgjr3bre3vvro7duzpz2j5tfrhwnarlqw6yq5cce6fo5qh3u7yq/{id}.json") Ownable(msg.sender){ 27 | // Mint one soulbound token to the contract creator (can be adjusted based on your use case) 28 | _mint(msg.sender, SOULBOUND_TOKEN_ID, 1, ""); 29 | soulboundTokenOwners[msg.sender] = true; 30 | } 31 | 32 | // Function to mint item tokens. Only callable by the contract owner. 33 | function mintItemTokens(address to, uint256[] memory itemIds, uint256[] memory amounts) external onlyOwner { 34 | // Check if the caller is the owner of the soulbound token 35 | require(soulboundTokenOwners[to], "No soulbound token found"); 36 | 37 | // Mint the item tokens 38 | _mintBatch(to, itemIds, amounts, ""); 39 | } 40 | 41 | // Function to transfer item tokens. Only callable by the owner of the soulbound token. 42 | function transferItemTokens(address from, address to, uint256[] memory itemIds, uint256[] memory amounts) external onlySoulboundTokenOwner { 43 | // Check if the recipient owns the soulbound token 44 | require(soulboundTokenOwners[to], "No soulbound token found"); 45 | 46 | // Transfer the item tokens 47 | _safeBatchTransferFrom(from, to, itemIds, amounts, ""); 48 | } 49 | 50 | // Function to check if an address owns the soulbound token 51 | function ownsSoulboundToken(address account) external view returns (bool) { 52 | return soulboundTokenOwners[account]; 53 | } 54 | 55 | // Function to transfer ownership of the soulbound token. Only callable by the contract owner. 56 | function transferSoulboundTokenOwnership(address newOwner) external onlyOwner { 57 | require(newOwner != address(0), "Invalid new owner address"); 58 | require(!soulboundTokenOwners[newOwner], "New owner already owns the soulbound token"); 59 | 60 | // Remove ownership from the current owner and assign it to the new owner 61 | soulboundTokenOwners[msg.sender] = false; 62 | soulboundTokenOwners[newOwner] = true; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /test/Soulbound.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.19; 3 | 4 | import {Test, console2} from "forge-std/Test.sol"; 5 | import {Soulbound} from "../src/Soulbound.sol"; 6 | 7 | contract SoulboundTest is Test { 8 | Soulbound public soulbound; 9 | 10 | address owner = mkaddr('owner'); 11 | address tester = mkaddr('tester'); 12 | 13 | function setUp() public { 14 | vm.startPrank(owner); 15 | soulbound = new Soulbound(); 16 | vm.stopPrank(); 17 | } 18 | 19 | function test_Mintitemtokens() public { 20 | vm.startPrank(owner); 21 | uint256[] memory ids = new uint[](3); 22 | assert(ids.length == 3); 23 | ids[0] = 2; 24 | ids[1] = 3; 25 | ids[2] = 4; 26 | assert(ids[0] == 2); 27 | assert(ids[1] == 3); 28 | assert(ids[2] == 4); 29 | uint256[] memory amount = new uint[](3); 30 | amount[0] = 1; 31 | amount[1] = 1; 32 | amount[2] = 1; 33 | assert(amount[0] == 1); 34 | assert(amount[1] == 1); 35 | assert(amount[2] == 1); 36 | soulbound.mintItemTokens(owner, ids, amount); 37 | soulbound.balanceOf(owner, 1); 38 | soulbound.balanceOf(owner, 2); 39 | assert(soulbound.balanceOf(owner, 1) == 1); 40 | assert(soulbound.balanceOf(owner, 2) == 1); 41 | assert(soulbound.balanceOf(owner, 3) == 1); 42 | assert(soulbound.balanceOf(owner, 4) == 1); 43 | vm.stopPrank(); 44 | } 45 | 46 | function test_Transferitemtokens() public { 47 | vm.startPrank(owner); 48 | uint256[] memory ids = new uint[](3); 49 | assert(ids.length == 3); 50 | ids[0] = 2; 51 | ids[1] = 3; 52 | ids[2] = 4; 53 | assert(ids[0] == 2); 54 | assert(ids[1] == 3); 55 | assert(ids[2] == 4); 56 | uint256[] memory amounts = new uint[](3); 57 | amounts[0] = 1; 58 | amounts[1] = 1; 59 | amounts[2] = 1; 60 | assert(amounts[0] == 1); 61 | assert(amounts[1] == 1); 62 | assert(amounts[2] == 1); 63 | soulbound.mintItemTokens(owner, ids, amounts); 64 | soulbound.transferItemTokens(owner, owner, ids, amounts); 65 | assert(soulbound.balanceOf(owner, 1) == 1); 66 | assert(soulbound.balanceOf(owner, 2) == 1); 67 | assert(soulbound.balanceOf(owner, 3) == 1); 68 | assert(soulbound.balanceOf(owner, 4) == 1); 69 | vm.stopPrank(); 70 | } 71 | 72 | function test_OwnsSoulboundToken() public { 73 | vm.startPrank(owner); 74 | soulbound.ownsSoulboundToken(owner); 75 | assert(soulbound.balanceOf(owner, 1) == 1); 76 | vm.stopPrank(); 77 | } 78 | 79 | function test_TransferSoulboundTokenOwnership() public { 80 | vm.startPrank(owner); 81 | soulbound.transferSoulboundTokenOwnership(tester); 82 | assert(soulbound.ownsSoulboundToken(tester) == true); 83 | vm.stopPrank(); 84 | } 85 | 86 | 87 | function mkaddr(string memory name) public returns (address) { 88 | address addr = address( 89 | uint160(uint256(keccak256(abi.encodePacked(name)))) 90 | ); 91 | vm.label(addr, name); 92 | return addr; 93 | } 94 | } 95 | --------------------------------------------------------------------------------