# A Step-by-Step Guide to Building a Decentralized Pet Adoption Marketplace on CELO Blockchain ## Table of Contents: 1. [Introduction](#Introduction) 2. [Smart contract design](#Smart-Contract-Design) 3. [Adding a pet to the shop](#Adding-pet-to-shop) 4. [Pet adoption process](#Adopt-Pet) 5. [Updating the general fee for pet adoption ](#The-updateFee-Function) 5. [Retrieving pet details](#The-getPet-function) 6. [Get contract balance](The-getContractEthBalance) 7. [Owner emergency withdrawal](#Withdraw-from-contract-address-to-owner-address) 8. [Complete Code](#Complete-Code) 9. [Setting up Remix for CELO ](#setting-up-remix-for-celo) ## Prerequisites - Basic understanding of Solidity programming language - Metamask Wallet [metamask.io](https://metamask.io/) - Access to Remix IDE [Remix](https://remix.ethereum.org) ## Introduction In this tutorial, you'll learn to develop a decentralized pet adoption marketplace using Solidity and Remix IDE. We'll set up Remix for deployment on the CELO Alfajores network, allowing you to deploy your pet adoption smart contract on the blockchain. Let's get started! ## Smart Contract Design ### Explanation of data structures ```solidity contract PetAdoptionMarketplace { address public owner; uint256 public generalFee; uint256 public constant maxPetsOwned = 3; struct Pet { address petAddress; address owner; uint256 age; string name; string breed; } mapping(address => Pet) public pets; mapping(address => uint256) public hasPaid; mapping(address => uint256) public petOwnershipCount; mapping(address => bool) public petsInShop; event PetAdopted(address indexed adopter, address indexed pet); event PetAddedToShop(address indexed owner, address indexed pet); event FeeUpdated(uint256 newFee); } ``` The `PetAdoptionMarketplace` smart contract serves as a decentralized marketplace for pet adoption. Leveraging blockchain technology, this contract facilitates transparent and secure transactions between pet owners and adopters. It provides functionality to store pet information, adopt pet, make payment, and retrieve a pet. The `owner` : An address variable to store the address of the owner of the contract. The `generalFee`: Represents the standard adoption fee that adopters are required to pay when they choose to adopt a pet. The `maxPetsOwned` : This denotes the maximum number of pets that a user is allowed to possess. The `Pet`: struct represents the data structure of pets in the shop, containing fields for address, name and age. The `mapping mapping(address => Pet) public pets;`: is used to store information about pets, where the key is the address of the pet owner, and the value is a Pet struct representing the details of the pet The `mapping mapping(address => uint256) public hasPaid;`: is used to track whether a user has paid the adoption fee for adopting a pet The `mapping mapping(address => uint256) public petOwnershipCount;`: is used to keep track of the number of pets owned by each user The `mapping mapping(address => bool) public petsInShop;`: is used to track whether a particular pet is available in the shop. ### Event description and modifiers ```solidity event PetAdopted(address indexed adopter, address indexed pet); ``` This event is emitted when a pet is successfully adopted on the platform. It returns the adopter's address (`adopter`) and the pet's address (`pet`). ```solidity event PetAddedToShop(address indexed owner, address indexed pet, string name, string breed, uint256 age); ``` This event is emitted when a new pet is added to the shop. It returns the address of the owner(`owner`), pet (`pet`), the name of the pet (`name`), and age(`age`). ```solidity event FeeUpdated(uint256 newFee); ``` This event is emitted when the general fee is updated and returns the newFee (`newFee`). ```solidity modifier onlyOwner() { require(msg.sender == owner, "Only owner can call this function"); _; } ``` The onlyOwner modifier grants exclusive privileges to the contract owner, allowing them to execute specific functions, such as administrative tasks or contract upgrades. When applied to a function, it ensures that only the owner can call that function. ```solidity constructor() { owner = msg.sender; } ``` Constructor is a special function that is executed only once during contract deployment. In this contract, the constructor initializes the contract owner to the address of the deployer (`msg.sender`). ## Adding pet to shop ### Implementation details of adding pet to shop functionality ```solidity /// @notice Adds a pet to the shop with the specified details. /// @dev Only the contract owner can add pets to the shop. /// @param _petAddress The address of the pet. /// @param _name The name of the pet. /// @param _breed The breed of the pet. /// @param _age The age of the pet. function addPetToShop( address _petAddress, string memory _name, string memory _breed, uint256 _age ) public onlyAdmin { // Ensure the name, breed and age of the pet are provided require(bytes(_name).length > 0, "Pet name is required"); require(bytes(_breed).length > 0, "Pet breed is required"); require(_age > 0, "Pet age is required"); // Mark the pet as available in the shop and add it to the pets mapping petsInShop[_petAddress] = true; pets[_petAddress] = Pet({ petAddress: _petAddress, owner: msg.sender, name: _name, breed: _breed, age: _age }); // Emit event to notify the addition of the pet to the shop emit PetAddedToShop(msg.sender, _petAddress, _name, _breed, _age); } ``` ### `addPetToShop` function This function `addPetToShop` allows the contract owner to add a new pet to the shop with the specified details, including the pet's address, name, and age. ## Adopt Pet ### Implementation details of adopting a pet ```solidity /// @notice Allows a user to adopt a pet from the shop by paying the adoption fee. /// @param _pet The address of the pet being adopted. function adoptPet(address _pet) external payable { address ownerAddress = msg.sender; // Check if the maximum number of pets owned by the caller is reached require( petOwnershipCount[ownerAddress] < maxPetsOwned, "Max pets owned" ); // Check if the pet is available in the shop require(petsInShop[_pet], "Pet not available in shop"); // Ensure the correct amount of Ether is sent as the adoption fee require(msg.value == generalFee * 1e18, "Incorrect adoption fee"); // Update pet ownership count petOwnershipCount[ownerAddress]++; // Mark pet as adopted petsInShop[_pet] = false; // Update payment record hasPaid[ownerAddress] = msg.value; // Transfer the adoption fee to the contract (bool success, ) = address(this).call{value: msg.value}(""); require(success, "Failed to complete pet adoption payment"); // Emit event to notify the adoption emit PetAdopted(ownerAddress, _pet); } ``` ### The `adoptPet` function This function adoptPet enables a user to adopt a pet from the shop by paying the specified adoption fee. - `payable`: The payable modifier allows users to send Ether along with the adoptPet function call. This is important because adopting a pet typically involves paying an adoption fee. By marking the function as payable, users can include the adoption fee as Ether when calling the function. The contract then receives this Ether and processes the adoption accordingly. - `msg.sender`: This is the address of the account that triggered the function call - `msg.value`: The amount of Ether (in wei) sent along with a function call - `address(this)`: This is the contract's address ### The `updateFee` function This function, `updateFee` enables the contract owner to modify the general fee required for pet adoption. ```solidity /// @dev Updates the general fee for pet adoption. /// @param _newFee The new adoption fee to be set. /// @notice This function allows the contract owner to update the general fee for pet adoption. function updateFee(uint256 _newFee) external onlyAdmin { generalFee = _newFee; emit FeeUpdated(_newFee); } ``` ### The `getPet` function This function retrieves information about a pet by it's address. ```solidity /// @dev Retrieves information about a pet by its address. /// @param _petAddress The address of the pet to retrieve information for. /// @return Pet struct containing details of the specified pet. function getPet(address _petAddress) public view returns (Pet memory) { require(_petAddress != address(0), "Invalid pet address"); return pets[_petAddress]; } ``` ## Retreiving contract balance ```solidity /// @dev Returns the Ether balance of the contract. /// @return The Ether balance of the contract. function getContractEthBalance() public view onlyAdmin returns (uint256) { return address(this).balance; } ``` ### The `getContractEthBalance` This function, getContractEthBalance, is a view function designed to retrieve the current Ether balance of the contract. It is annotated with @dev to indicate that it provides internal developer documentation - `onlyAdmin`: This modifier ensures that only shop owner/admin can call this function to view contract balance. - `address(this)`: Remember, when adopting a pet, the adoption fee is sent to the contract's address itself *`address(this)`.* To retrieve the balance, we simply refer to the contract's address ### Withdraw from contract address to owner address This function allows the contract owner to perform an emergency withdrawal of Ether from the contract's balance to the owner's address ```solidity /// @dev Allows the contract owner to perform an emergency withdrawal of Ether. /// @param amount The amount of Ether to withdraw. /// @notice This function allows the contract owner to withdraw Ether from the contract. function withdrawEther(uint256 amount) external onlyAdmin { require(msg.sender != address(0), "Sender address is zero"); require(amount > 0, "Invalid amount"); // Convert amount to wei uint256 amountInWei = amount * 1e18; // Transfer Ether to the contract owner address (bool success, ) = msg.sender.call{value: amountInWei}(""); require(success, "Failed to withdraw Ether"); } ``` `onlyAdmin`: This is to ensure that only owner/admin can perform emergency withdrawal. ### The `recieve` function The `receive` function in this Solidity contract acts as a default method to handle incoming Ether transfers without needing a specific function call. It ensures that funds sent to the contract are correctly processed, making interactions seamless for users who can send Ether without explicit function calls. ```solidity /// @dev Fallback function to receive Ether. /// @notice This function is called when Ether is sent to the contract without specifying a function. /// It logs a message indicating that Ether has been received. receive() external payable { console.log( "RECEIVE: ETH received" ); } ``` ## Complete Code This is the complete code for this decentralized social contract. ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "hardhat/console.sol"; contract PetShop { address public owner; uint256 public generalFee; uint256 public constant maxPetsOwned = 3; struct Pet { address owner; address petAddress; uint256 age; string name; } mapping(address => Pet) public pets; mapping(address => uint256) public hasPaid; mapping(address => uint256) public petOwnershipCount; mapping(address => bool) public petsInShop; event PetAdopted(address indexed adopter, address indexed pet); event PetAddedToShop( address indexed owner, address indexed pet, string name, uint256 age ); event FeeUpdated(uint256 newFee); // @notice Constructs the PetShop contract with the specified general fee and sets the contract owner. /// @param _generalFee The general adoption fee for pets in the shop. constructor(uint256 _generalFee) { owner = msg.sender; // Set the contract owner to the sender of the transaction generalFee = _generalFee; // Set the general adoption fee } /// @notice Modifier to restrict access to functions only to the contract owner (admin). modifier onlyAdmin() { require(msg.sender == owner, "Only admin can call this function"); // Ensure only the contract owner can call the function _; // Continue with the function execution } /// @notice Adds a pet to the shop with the specified details. /// @dev Only the contract owner can add pets to the shop. /// @param _petAddress The address of the pet. /// @param _name The name of the pet. /// @param _age The age of the pet. function addPetToShop( address _petAddress, string memory _name, uint256 _age ) public onlyAdmin { // Ensure the name and age of the pet are provided require(bytes(_name).length > 0, "Pet name is required"); require(_age > 0, "Pet age is required"); // Mark the pet as available in the shop and add it to the pets mapping petsInShop[_petAddress] = true; pets[_petAddress] = Pet({ petAddress: _petAddress, owner: msg.sender, name: _name, age: _age }); // Emit event to notify the addition of the pet to the shop emit PetAddedToShop(msg.sender, _petAddress, _name, _age); } /// @notice Allows a user to adopt a pet from the shop by paying the adoption fee. /// @param _pet The address of the pet being adopted. function adoptPet(address _pet) external payable { address ownerAddress = msg.sender; // Check if the maximum number of pets owned by the caller is reached require( petOwnershipCount[ownerAddress] < maxPetsOwned, "Max pets owned" ); // Check if the pet is available in the shop require(petsInShop[_pet], "Pet not available in shop"); // Ensure the correct amount of Ether is sent as the adoption fee require(msg.value == generalFee * 1e18, "Incorrect adoption fee"); // Update pet ownership count, mark pet as adopted, and record payment petOwnershipCount[ownerAddress]++; petsInShop[_pet] = false; hasPaid[ownerAddress] = msg.value; // Transfer the adoption fee to the contract (bool success, ) = address(this).call{value: msg.value}(""); require(success, "Failed to complete pet adoption payment"); // Emit event to notify the adoption emit PetAdopted(ownerAddress, _pet); } function updateFee(uint256 _newFee) external onlyAdmin { generalFee = _newFee; emit FeeUpdated(_newFee); } /// @dev Returns the Ether balance of the contract. /// @return The Ether balance of the contract. function getContractEthBalance() public view onlyAdmin returns (uint256) { return address(this).balance; } /// @dev Allows the contract owner to perform an emergency withdrawal of Ether. /// @param amount The amount of Ether to withdraw. /// @notice This function allows the contract owner to withdraw Ether from the contract. function withdrawEther(uint256 amount) external onlyAdmin { require(msg.sender != address(0), "Sender address is zero"); require(amount > 0, "Invalid amount"); // Convert amount to wei uint256 amountInWei = amount * 1e18; // Transfer Ether to the contract owner address (bool success, ) = msg.sender.call{value: amountInWei}(""); require(success, "Failed to withdraw Ether"); } /// @dev Retrieves information about a pet by its address. /// @param _petAddress The address of the pet to retrieve information for. /// @return Pet struct containing details of the specified pet. function getPet(address _petAddress) public view returns (Pet memory) { require(_petAddress != address(0), "Invalid pet address"); return pets[_petAddress]; } /// @dev Fallback function to receive Ether. /// @notice This function is called when Ether is sent to the contract without specifying a function. /// It logs a message indicating that Ether has been received. receive() external payable { console.log( "RECEIVE: ETH received" ); } } ``` ## Setting up Remix for CELO ### Open Remix IDE and paste code Go to your web browser and open [remix IDE](https://remix.ethereum.org). You should see a similar interface like below : ![Screenshot 2024-03-31 at 23.02.02](https://hackmd.io/_uploads/Hya5UPDJC.png) Click on `contracts` folder and create a new file name it `PetAdoptionShop.sol` then paste the full code in the file. ![Screenshot 2024-03-31 at 23.08.43](https://hackmd.io/_uploads/B106evPkC.png) ### Compile and deploy On the left tab, click on `compile` icon. ![Screenshot 2024-03-31 at 23.08.43](https://hackmd.io/_uploads/SkNx4wPkC.png) Next, click on `compile PetAdoptionShop.sol` to compile your contract. ![f-compile] ![Screenshot 2024-03-31 at 23.11.14](https://hackmd.io/_uploads/Bkpf4vDyR.png) Upon successful compilation of your contracts, you should see a green checkmark like below : ![Screenshot 2024-03-31 at 23.12.11](https://hackmd.io/_uploads/Hy-a4Dwy0.png) Now to proceed, ensure you have connected Metamask or the provider you are using to CELO alfajores. You can follow this [guide](https://medium.com/defi-for-the-people/how-to-set-up-metamask-with-celo-912d698fcafe) to connect CELO to Metamask. Next, select `injected provider ` under your environments to connect to Metamask and click on `deploy` button. ![Screenshot 2024-03-31 at 23.45.51](https://hackmd.io/_uploads/H1ZeOPPy0.png) Upon successful deployment of your contract, your interface should look like below : ![Screenshot 2024-03-31 at 23.38.12](https://hackmd.io/_uploads/BJGfLwwJR.png) Congratulations! You have successfully written a decentralized pet adoption marketplace smart contract and deployed it to CELO on Remix