Multi-Tier Staking Reward Calculator JavaScript, Solidity
👤 Sharing: AI
Okay, let's craft a multi-tier staking reward calculator using JavaScript and Solidity. This example will illustrate a simplified version of how such a system could work.
**Solidity (Smart Contract - `StakingContract.sol`)**
```solidity
pragma solidity ^0.8.0;
contract StakingContract {
// State Variables
address public owner;
uint256 public totalStaked;
struct StakerInfo {
uint256 stakeAmount;
uint256 startTime;
uint8 tier; // Staking tier
}
mapping(address => StakerInfo) public stakers;
// Staking Tiers and APR (Annual Percentage Rate)
uint8 public constant NUM_TIERS = 3; // Define number of tiers
uint256[NUM_TIERS] public aprs = [5, 10, 15]; // APRs for each tier (in percentage, i.e. 5 = 5%)
uint256[NUM_TIERS] public stakingMinimums = [100, 500, 1000]; // Minimum stake required for each tier
// Event
event Staked(address staker, uint256 amount, uint8 tier);
event Unstaked(address staker, uint256 amount, uint256 reward);
// Modifier to restrict access to the contract owner
modifier onlyOwner() {
require(msg.sender == owner, "Only owner can call this function");
_;
}
// Constructor
constructor() {
owner = msg.sender;
}
// Function to set APRs for each tier. Only owner can call this function
function setAprs(uint256[NUM_TIERS] memory _aprs) public onlyOwner {
aprs = _aprs;
}
// Function to set minimum staking amounts for each tier. Only owner can call this function
function setStakingMinimums(uint256[NUM_TIERS] memory _stakingMinimums) public onlyOwner {
stakingMinimums = _stakingMinimums;
}
// Function to stake tokens
function stake(uint256 _amount) public {
require(_amount > 0, "Amount must be greater than zero");
require(stakers[msg.sender].stakeAmount == 0, "Already staking");
// Determine the tier based on the staking amount
uint8 tier = determineTier(_amount);
// Update staker information
stakers[msg.sender] = StakerInfo({
stakeAmount: _amount,
startTime: block.timestamp,
tier: tier
});
totalStaked += _amount;
// Emit event
emit Staked(msg.sender, _amount, tier);
}
// Internal function to determine the tier based on the staking amount
function determineTier(uint256 _amount) internal view returns (uint8) {
for (uint8 i = NUM_TIERS - 1; i > 0; i--) {
if (_amount >= stakingMinimums[i]) {
return i;
}
}
return 0; // Default to tier 0 if amount is less than all minimums
}
// Function to calculate reward
function calculateReward(address _staker) public view returns (uint256) {
StakerInfo storage staker = stakers[_staker];
require(staker.stakeAmount > 0, "Not staking");
uint256 timeStaked = block.timestamp - staker.startTime;
uint256 apr = aprs[staker.tier];
// Calculate reward based on APR and time staked. Simple calculation for example.
// (stakeAmount * apr * timeStaked) / (100 * 365 days in seconds)
return (staker.stakeAmount * apr * timeStaked) / (100 * 365 * 1 days); //Adjust for day calculation.
}
// Function to unstake tokens
function unstake() public {
StakerInfo storage staker = stakers[msg.sender];
require(staker.stakeAmount > 0, "Not staking");
uint256 reward = calculateReward(msg.sender);
uint256 amount = staker.stakeAmount;
// Reset staker info
delete stakers[msg.sender]; //Resets the struct to default values.
totalStaked -= amount;
// Emit event
emit Unstaked(msg.sender, amount, reward);
// Normally you would transfer the amount and reward to the staker here.
// For this example, we'll just leave a comment.
// transferToken(msg.sender, amount + reward);
}
// In a real-world scenario, you would have a function to transfer tokens
// from the contract to the user. This is a placeholder.
// function transferToken(address _to, uint256 _amount) internal {
// // Actual transfer logic would go here
// }
//Optional functions to read contract state.
function getStakerInfo(address _staker) public view returns (uint256 stakeAmount, uint256 startTime, uint8 tier) {
StakerInfo memory staker = stakers[_staker];
return (staker.stakeAmount, staker.startTime, staker.tier);
}
function getAprForTier(uint8 _tier) public view returns (uint256) {
require(_tier < NUM_TIERS, "Invalid tier");
return aprs[_tier];
}
function getStakingMinimumForTier(uint8 _tier) public view returns (uint256) {
require(_tier < NUM_TIERS, "Invalid tier");
return stakingMinimums[_tier];
}
}
```
**Explanation (Solidity):**
1. **`pragma solidity ^0.8.0;`**: Specifies the Solidity compiler version.
2. **`contract StakingContract { ... }`**: Defines the smart contract.
3. **State Variables:**
* `owner`: Address of the contract owner (who can set APRs).
* `totalStaked`: The total amount staked in the contract.
* `StakerInfo` struct: Stores information about each staker (stake amount, start time, tier).
* `stakers` mapping: Maps staker addresses to their `StakerInfo`.
* `NUM_TIERS`: Constant that defines the number of tiers available
* `aprs`: Array that stores the Annual Percentage Rates (APRs) for each tier.
* `stakingMinimums`: Array that stores the minimum staking amounts for each tier.
4. **Events:** `Staked` and `Unstaked` events are emitted when staking or unstaking occurs. These are essential for tracking activity on the blockchain.
5. **`onlyOwner` Modifier:** A modifier to restrict certain functions to only be callable by the contract owner.
6. **`constructor()`**: Sets the `owner` upon contract deployment.
7. **`setAprs(uint256[NUM_TIERS] memory _aprs)`**: Sets the APR values for each staking tier.
8. **`setStakingMinimums(uint256[NUM_TIERS] memory _stakingMinimums)`**: Sets the minimum staking amount for each tier.
9. **`stake(uint256 _amount)`**:
* Allows users to stake tokens.
* Determines the staking tier based on the amount staked using the `determineTier` function.
* Updates the `stakers` mapping with the stake amount, start time, and tier.
* Updates `totalStaked`.
* Emits the `Staked` event.
10. **`determineTier(uint256 _amount)`**: An internal function that determines the staking tier based on the staked amount by comparing to `stakingMinimums`.
11. **`calculateReward(address _staker)`**: Calculates the reward based on the staked amount, tier, and the time the tokens have been staked. The reward calculation is simplified for this example.
12. **`unstake()`**:
* Allows users to unstake their tokens.
* Calculates the reward using `calculateReward()`.
* Resets the staker's information in the `stakers` mapping.
* Updates `totalStaked`.
* Emits the `Unstaked` event.
* *Important:* In a real implementation, this function would also handle the transfer of the staked tokens and the accrued rewards back to the staker. This example only includes a comment where the token transfer logic would go.
13. **`getStakerInfo(address _staker)`**: A public view function to retrieve information about a staker.
14. **`getAprForTier(uint8 _tier)`**: A public view function to retrieve the APR for a given tier.
15. **`getStakingMinimumForTier(uint8 _tier)`**: A public view function to retrieve the minimum staking amount for a given tier.
**JavaScript (Frontend Interaction - `app.js`)**
```javascript
// Needs to be run in an environment that supports ethers.js (e.g., Node.js with ethers installed, or a browser with MetaMask)
const { ethers } = require("ethers");
// Replace with your contract address and ABI
const contractAddress = "YOUR_CONTRACT_ADDRESS"; // Replace with your actual contract address
const contractABI = [
// Paste your contract ABI here (from Solidity compiler output)
{
"inputs": [],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "staker",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint8",
"name": "tier",
"type": "uint8"
}
],
"name": "Staked",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "staker",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "reward",
"type": "uint256"
}
],
"name": "Unstaked",
"type": "event"
},
{
"inputs": [
{
"internalType": "address",
"name": "_staker",
"type": "address"
}
],
"name": "calculateReward",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint8",
"name": "_tier",
"type": "uint8"
}
],
"name": "getAprForTier",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint8",
"name": "_tier",
"type": "uint8"
}
],
"name": "getStakingMinimumForTier",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "_staker",
"type": "address"
}
],
"name": "getStakerInfo",
"outputs": [
{
"internalType": "uint256",
"name": "stakeAmount",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "startTime",
"type": "uint256"
},
{
"internalType": "uint8",
"name": "tier",
"type": "uint8"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "_amount",
"type": "uint256"
}
],
"name": "stake",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "unstake",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
];
async function main() {
// Connect to a provider (e.g., Infura, Alchemy, or a local Ganache instance)
const provider = new ethers.providers.JsonRpcProvider("YOUR_PROVIDER_URL"); // Replace with your provider URL
// Get a signer (e.g., from MetaMask) - you'll need to unlock your wallet
const signer = new ethers.Wallet("YOUR_PRIVATE_KEY", provider); // Replace with your private key, for testing purposes only. NEVER hardcode private keys in production.
// Create a contract instance
const stakingContract = new ethers.Contract(contractAddress, contractABI, signer);
// Example usage:
// 1. Stake tokens
const stakeAmount = ethers.utils.parseEther("600"); // Stake 600 tokens (adjust amount as needed)
console.log(`Staking ${ethers.utils.formatEther(stakeAmount)} tokens...`);
const stakeTx = await stakingContract.stake(stakeAmount);
await stakeTx.wait(); // Wait for the transaction to be mined
console.log("Staking transaction confirmed!");
// 2. Get Staker Info
const stakerInfo = await stakingContract.getStakerInfo(signer.address);
console.log("Staker Info:", {
stakeAmount: ethers.utils.formatEther(stakerInfo.stakeAmount),
startTime: stakerInfo.startTime.toString(),
tier: stakerInfo.tier.toString(),
});
// 3. Calculate Reward
const reward = await stakingContract.calculateReward(signer.address);
console.log("Estimated Reward:", ethers.utils.formatEther(reward), "tokens");
// 4. Unstake tokens
console.log("Unstaking tokens...");
const unstakeTx = await stakingContract.unstake();
await unstakeTx.wait();
console.log("Unstaking transaction confirmed!");
}
main().catch((error) => {
console.error(error);
});
```
**Explanation (JavaScript):**
1. **Dependencies:** Requires the `ethers` library to interact with the Ethereum blockchain. You'll need to install it: `npm install ethers`.
2. **Configuration:**
* `contractAddress`: Replace `"YOUR_CONTRACT_ADDRESS"` with the actual address of your deployed `StakingContract` on the blockchain.
* `contractABI`: Replace the placeholder with the ABI (Application Binary Interface) of your `StakingContract`. You'll get this when you compile your Solidity code. It describes the contract's functions and data structures.
* `provider`: Replace `"YOUR_PROVIDER_URL"` with the URL of an Ethereum provider. This could be:
* Infura: A popular hosted Ethereum API service (you'll need an account and API key).
* Alchemy: Another hosted Ethereum API service.
* A local Ganache instance: If you're developing locally.
* `signer`: Replace `"YOUR_PRIVATE_KEY"` with the private key of an Ethereum account that you want to use to interact with the contract. **Important:** *Never* hardcode private keys in production code. Use a secure way to manage private keys (e.g., a hardware wallet or a secure vault). For testing, you can use a private key generated by Ganache.
3. **`main()` function:**
* Connects to the Ethereum provider using `ethers.providers.JsonRpcProvider()`.
* Creates a `signer` using `ethers.Wallet()`. The `signer` is the account that will be used to send transactions to the contract.
* Creates a `stakingContract` instance using `new ethers.Contract()`. This creates a JavaScript object that represents your smart contract.
* **Example Usage:** The code then demonstrates how to call various functions of the contract:
* `stake()`: Stakes a specified amount of tokens. `ethers.utils.parseEther()` is used to convert the amount from a human-readable format (e.g., "1.0") to the smallest unit of Ether (Wei).
* `getStakerInfo()`: Retrieves information about a staker (stake amount, start time, tier).
* `calculateReward()`: Calculates the estimated reward for a staker.
* `unstake()`: Unstakes the tokens and claims the reward.
* `ethers.utils.formatEther()` is used to convert the amounts from Wei back to a human-readable Ether format.
4. **Error Handling:** The `main()` function is wrapped in a `try...catch` block to handle any errors that might occur.
**To Run This Example:**
1. **Set up your environment:**
* Install Node.js and npm.
* Install `ethers`: `npm install ethers`.
* Install hardhat: `npm install --save-dev hardhat`
2. **Compile and Deploy the Solidity Contract:**
* You'll need to use a tool like Hardhat, Remix, or Truffle to compile and deploy the `StakingContract.sol` smart contract to a blockchain (e.g., a local Ganache instance, a testnet like Goerli, or the mainnet).
* **Get the Contract Address and ABI:** After deployment, you'll get the address of the deployed contract and its ABI. You'll need these values for the JavaScript code.
3. **Configure `app.js`:**
* Update `contractAddress`, `contractABI`, `provider`, and `signer` in `app.js` with the correct values.
4. **Run the JavaScript Code:**
* Execute the JavaScript code using Node.js: `node app.js`.
**Important Considerations:**
* **Security:** This is a simplified example and should *not* be used in a production environment without thorough security audits and testing. Smart contracts are vulnerable to attacks, so security is paramount.
* **Token Transfers:** The `transferToken()` function in the Solidity code is a placeholder. In a real implementation, you would need to integrate with an ERC-20 token contract to transfer tokens into the staking contract and back to the stakers.
* **Gas Costs:** Smart contract operations cost gas. Be mindful of gas costs when designing your staking system. Optimize your code to minimize gas usage.
* **Time Calculations:** The time calculations in this example are simplified. For more accurate reward calculations, you might need to consider block timestamps and potential time zone differences.
* **Reentrancy Attacks:** Staking contracts are particularly vulnerable to reentrancy attacks. Implement reentrancy guards to protect against such attacks.
* **Decimals:** Remember to account for the number of decimals used by the token you're staking. Many ERC-20 tokens have 18 decimals.
This provides a foundational example. Remember to thoroughly research and adapt this code to your specific needs, considering security and best practices for smart contract development. Good luck!
👁️ Viewed: 9
Comments