Skip to content

Instantly share code, notes, and snippets.

@veektrie
Created November 2, 2024 06:39
Show Gist options
  • Save veektrie/12b4be5e0313941638cd7ad81ed11665 to your computer and use it in GitHub Desktop.
Save veektrie/12b4be5e0313941638cd7ad81ed11665 to your computer and use it in GitHub Desktop.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
// Import Chainlink's AggregatorV3Interface for retrieving off-chain data
import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
// Import OpenZeppelin's Ownable for ownership management
import "@openzeppelin/contracts/access/Ownable.sol";
// Import OpenZeppelin's ReentrancyGuard for security against reentrancy attacks
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
/**
* @title SupplyChain
* @dev A simple supply chain management contract that allows the owner to add, update, and manage products.
* Utilizes Chainlink oracles for fetching the latest price data.
*/
contract SupplyChain is Ownable, ReentrancyGuard {
// Chainlink price feed interface
AggregatorV3Interface internal priceFeed;
// Enum to represent the product status
enum Status { Created, Shipped, Delivered, Canceled }
// Struct to represent products in the supply chain
struct Product {
string name; // Name of the product
uint256 price; // Price of the product (normalized to 18 decimals)
uint256 quantity; // Available quantity with the manufacturer
Status status; // Current status of the product
bool exists; // Flag to check if the product exists
}
// Mapping of product ID to Product struct
mapping(uint256 => Product) public products;
// Events for logging actions
event ProductAdded(
uint256 indexed productId,
string name,
uint256 price,
uint256 quantity,
address indexed addedBy
);
event ProductUpdated(
uint256 indexed productId,
uint256 price,
uint256 quantity,
Status status,
address indexed updatedBy
);
event ProductStatusUpdated(
uint256 indexed productId,
Status status,
address indexed updatedBy
);
event FundsWithdrawn(address indexed owner, uint256 amount);
/**
* @dev Constructor to set the Chainlink price feed address and initialize ownership.
* @param _priceFeed Address of the Chainlink price feed contract.
*/
constructor(address _priceFeed) Ownable(msg.sender) {
require(_priceFeed != address(0), "Invalid price feed address");
priceFeed = AggregatorV3Interface(_priceFeed);
}
/**
* @dev Adds a new product to the supply chain.
* @param productId Unique identifier for the product.
* @param name Name of the product.
* @param quantity Initial quantity of the product.
*
* Requirements:
* - Only the contract owner can call this function.
* - The product ID must be unique.
* - The product name cannot be empty.
*/
function addProduct(
uint256 productId,
string memory name,
uint256 quantity
) external onlyOwner {
require(!products[productId].exists, "Product already exists");
require(bytes(name).length > 0, "Product name cannot be empty");
uint256 price = getLatestPrice();
products[productId] = Product(name, price, quantity, Status.Created, true);
emit ProductAdded(productId, name, price, quantity, msg.sender);
}
/**
* @dev Updates the price and quantity of an existing product.
* @param productId Identifier of the product to update.
* @param quantity New quantity of the product.
*
* Requirements:
* - Only the contract owner can call this function.
* - The product must exist.
*/
function updateProduct(uint256 productId, uint256 quantity) external onlyOwner {
require(products[productId].exists, "Product does not exist");
uint256 price = getLatestPrice();
products[productId].price = price;
products[productId].quantity = quantity;
emit ProductUpdated(productId, price, quantity, products[productId].status, msg.sender);
}
/**
* @dev Updates the status of a product.
* @param productId Identifier of the product.
* @param status New status of the product.
*
* Requirements:
* - Only the contract owner can call this function.
* - The product must exist.
*/
function updateProductStatus(uint256 productId, Status status) external onlyOwner {
require(products[productId].exists, "Product does not exist");
products[productId].status = status;
emit ProductStatusUpdated(productId, status, msg.sender);
}
/**
* @dev Retrieves the latest price from the Chainlink oracle and normalizes it to 18 decimals.
* @return Normalized latest price.
*/
function getLatestPrice() internal view returns (uint256) {
(
,
int256 price,
,
,
) = priceFeed.latestRoundData();
require(price > 0, "Invalid price data");
uint8 decimals = priceFeed.decimals();
return uint256(price) * (10 ** (18 - decimals));
}
/**
* @dev Withdraws all Ether from the contract to the owner's address.
*
* Requirements:
* - Only the contract owner can call this function.
* - The contract must have a positive balance.
*/
function withdrawFunds() external onlyOwner nonReentrant {
uint256 balance = address(this).balance;
require(balance > 0, "No funds to withdraw");
(bool success, ) = payable(owner()).call{value: balance}("");
require(success, "Withdrawal failed");
emit FundsWithdrawn(owner(), balance);
}
/**
* @dev Fallback function to accept Ether.
*/
receive() external payable {}
/**
* @dev Fallback function to handle unexpected calls.
*/
fallback() external payable {}
}
Imagine you're working on a project to manage products in a store using a smart contract. You have an original version of the contract and an updated version that fixes some issues and adds new features. Let's break down the differences between these two versions in simple terms.
1. Using Trusted Tools for Ownership and Security
Original Contract:
- How It Worked: The contract kept track of who owned it by storing the owner's address in a variable called `owner`. It also created special rules (called "modifiers") to ensure that only the owner could perform certain actions, like adding or updating products.
- Problem: Managing ownership manually can be risky and error-prone. If there's a mistake in the ownership logic, it could lead to security vulnerabilities.
Updated Contract:
- What Changed: Instead of managing ownership from scratch, the updated contract uses a trusted library called OpenZeppelin. Specifically, it inherits from OpenZeppelin’s `Ownable` contract.
- Why It’s Better: OpenZeppelin’s tools are well-tested and widely used, making ownership management safer and easier. This reduces the chances of introducing bugs related to ownership.
2. Better Protection Against Hackers
Original Contract:
- How It Worked: The contract had a basic way to handle withdrawing money (Ether) from the contract. However, it didn’t include extra safety measures to protect against certain types of attacks.
- Problem: Without additional protections, the contract could be vulnerable to attacks where hackers trick the contract into sending more money than intended.
Updated Contract:
- What Changed: The updated contract adds an extra layer of protection against reentrancy attacks by inheriting from OpenZeppelin’s `ReentrancyGuard` contract.
- Why It’s Better: This ensures that when money is taken out of the contract, it can’t be tricked into giving out more money than it should. It makes the withdrawal process more secure.
3. More Accurate Price Handling
Original Contract:
- How It Worked: The contract fetched prices from an external source called a Chainlink oracle. However, it didn’t adjust these prices based on how they were provided, which could lead to mistakes.
- Problem: If the price data has decimals (like 8 decimal places), not adjusting for them can cause inaccurate price calculations.
Updated Contract:
- What Changed: The updated contract correctly adjusts the price by considering how many decimal places the price data has.
- Why It’s Better: This ensures that the prices are always accurate and consistent, avoiding any calculation errors.
4. Clearer and More Detailed Logs
Original Contract:
- How It Worked: The contract logged actions like adding, updating, or changing the status of products. However, it didn’t record who made these changes.
- Problem: Without knowing who performed each action, it’s hard to track accountability and understand who is responsible for specific changes.
Updated Contract:
- What Changed: The updated contract now also records the address (like an account name) of the person who added or updated a product.
- Why It’s Better: This helps keep track of who did what, making everything more transparent and easier to audit.
5. Safer Money Withdrawals
Original Contract:
- How It Worked: Used a simple method to send money to the owner, which might not work well with all types of accounts.
- Problem: Some accounts, especially smart contracts, might require more gas to receive funds, causing the transfer to fail.
Updated Contract:
- What Changed: Uses a more reliable method called `call` instead of `transfer` to send money.
- Why It’s Better: This ensures that the money transfer happens successfully and safely, even with different types of accounts.
6. Ability to Receive Money Directly
Original Contract:
- How It Worked: Didn’t have a way to handle money sent directly to it without calling a specific function.
- Problem: If someone tries to send money to the contract without using the designated functions, the transaction would fail or be lost.
Updated Contract:
- What Changed: Includes special functions called `receive` and `fallback` that allow the contract to accept money sent directly to it.
- Why It’s Better: This makes the contract more flexible and ready for future features, like selling products for money, without losing any funds.
7. Better Documentation and Easy-to-Understand Messages
Original Contract:
- How It Worked: Had basic comments explaining what each part did.
- Problem: The comments were minimal, making it harder for someone new to the code to understand how everything works.
Updated Contract:
- What Changed: Adds more detailed explanations (called NatSpec comments) and clearer error messages.
- Why It’s Better: This makes it easier for anyone reading the code to understand how it works and what each part does, improving readability and maintainability.
8. Optimized Structure for Efficiency
Original Contract:
- How It Worked: Organized the information about each product in a straightforward way.
- Problem: The way data was stored could be optimized to use less computer power, which in blockchain terms means using less gas (cost).
Updated Contract:
- What Changed: Slightly rearranged how the product information is stored to make things run a bit more smoothly and use less computer power.
- Why It’s Better: This can save money when using the contract, making it more efficient and cost-effective.
---
Why These Changes Matter
- Security: Using trusted libraries and adding extra protections makes the contract safer from hackers.
- Accuracy: Properly handling price data ensures that everything stays correct and reliable.
- Transparency: Keeping track of who does what helps everyone trust the system more.
- Flexibility: Being able to receive money directly and having better documentation makes it easier to add new features in the future.
---
In Summary
The updated `SupplyChain` contract improves upon the original by making it more secure, accurate, and easier to use. It does this by:
- Using trusted tools from OpenZeppelin for managing ownership and security.
- Adding protections against certain types of cyber-attacks.
- Ensuring that price data is handled correctly.
- Keeping detailed logs of who makes changes.
- Making money withdrawals safer and more reliable.
- Allowing the contract to accept money sent directly to it.
- Providing better documentation and clearer error messages.
- Optimizing the structure to save on costs.
All these changes help the contract work better in real-world scenarios without making it too complicated. This means it’s more reliable, secure, and ready to handle various challenges while still being easy to understand and use.
---
Learning Points
1. Use Trusted Libraries: Instead of writing everything from scratch, use well-tested libraries like OpenZeppelin to handle common tasks. This saves time and reduces the risk of bugs.
2. Think About Security: Always consider potential vulnerabilities and add protections to safeguard your contract from attacks.
3. Handle Data Correctly: Pay attention to how data is formatted and used, especially when dealing with external sources like price oracles.
4. Keep Logs Detailed: Recording who does what helps in tracking actions and maintaining accountability.
5. Optimize for Efficiency: Organize your data structures to use less gas, making your contract cheaper to use.
6. Document Your Code: Clear comments and explanations make your code easier to understand and maintain, especially for others who might work with it.
By understanding these changes and why they were made, you can write smarter, safer, and more efficient smart contracts in the future!
---
If you have any questions or need further clarification on any of these points, feel free to ask!
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment