Multi-Tier Staking Reward Calculator JavaScript, Solidity
👤 Sharing: AI
Okay, here's an example implementation of a multi-tier staking reward calculator, combining JavaScript for a simple user interface (simulated) and Solidity for the core logic of the staking contract.
**Important Considerations:**
* **Security:** This is a simplified example. Real-world staking contracts require rigorous security audits to prevent vulnerabilities like reentrancy attacks, integer overflows/underflows, and other exploits. Always use established and audited libraries (e.g., OpenZeppelin) in production environments.
* **Gas Optimization:** Solidity contracts can become expensive due to gas costs. Careful design and optimization are crucial.
* **Precision:** Solidity doesn't natively support floating-point numbers. Calculations often involve fixed-point arithmetic or integer approximations. This example uses basic integer math.
* **Scalability:** Consider the impact of a large number of stakers on the contract's performance.
**Solidity Contract (MultiTierStaking.sol):**
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract MultiTierStaking {
struct Staker {
uint256 amount;
uint256 startTime;
uint8 tier;
}
mapping(address => Staker) public stakers;
IERC20 public token; // Address of the staking token
IERC20 public rewardToken; // Address of the reward token
uint256 public totalStaked;
//Tier Definitions
struct Tier {
uint8 tierId;
uint256 apr; // annual percentage rate (as a percentage 1000 = 10%)
uint256 minStake;
}
Tier[] public tiers;
uint256 public constant SECONDS_IN_YEAR = 365 days;
event Staked(address indexed user, uint256 amount, uint8 tier);
event Unstaked(address indexed user, uint256 amount);
event RewardPaid(address indexed user, uint256 amount);
constructor(address _tokenAddress, address _rewardTokenAddress) {
token = IERC20(_tokenAddress);
rewardToken = IERC20(_rewardTokenAddress);
}
// Define a Tier
function addTier(uint8 _tierId, uint256 _apr, uint256 _minStake) external {
require(tiers.length < 5, "Max Tiers reached"); //Limit number of tiers.
tiers.push(Tier(_tierId, _apr, _minStake));
}
function stake(uint256 _amount, uint8 _tierId) external {
require(stakers[msg.sender].amount == 0, "Already staked");
require(_amount > 0, "Amount must be greater than zero");
Tier memory selectedTier = getTier(_tierId);
require(_amount >= selectedTier.minStake, "Insufficient amount for selected tier");
token.transferFrom(msg.sender, address(this), _amount);
stakers[msg.sender] = Staker(_amount, block.timestamp, _tierId);
totalStaked += _amount;
emit Staked(msg.sender, _amount, _tierId);
}
function unstake() external {
Staker storage staker = stakers[msg.sender];
require(staker.amount > 0, "Not staked");
uint256 reward = calculateReward(msg.sender);
uint256 amount = staker.amount;
delete stakers[msg.sender];
totalStaked -= amount;
token.transfer(msg.sender, amount);
rewardToken.transfer(msg.sender, reward);
emit Unstaked(msg.sender, amount);
emit RewardPaid(msg.sender, reward);
}
function calculateReward(address _user) public view returns (uint256) {
Staker storage staker = stakers[_user];
require(staker.amount > 0, "Not staked");
Tier memory selectedTier = getTier(staker.tier);
uint256 timeElapsed = block.timestamp - staker.startTime;
uint256 reward = (staker.amount * selectedTier.apr * timeElapsed) / (1000 * SECONDS_IN_YEAR); // APR is a percentage
return reward;
}
function getTier(uint8 _tierId) internal view returns (Tier memory){
for(uint i = 0; i < tiers.length; i++){
if(tiers[i].tierId == _tierId){
return tiers[i];
}
}
revert("Tier does not exist.");
}
}
interface IERC20 {
function totalSupply() external view returns (uint256);
function balanceOf(address account) external view returns (uint256);
function transfer(address recipient, uint256 amount) external returns (bool);
function allowance(address owner, address spender) external view returns (uint256);
function approve(address spender, uint256 amount) external returns (bool);
function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
}
```
**Explanation of Solidity Contract:**
1. **`pragma solidity ^0.8.0;`**: Specifies the Solidity compiler version.
2. **`contract MultiTierStaking { ... }`**: Defines the staking contract.
3. **`struct Staker { ... }`**: A struct to hold information about each staker:
* `amount`: The amount of tokens staked.
* `startTime`: The block timestamp when the staking began.
* `tier`: The tier the user is staking in
4. **`mapping(address => Staker) public stakers;`**: A mapping from Ethereum address to `Staker` structs, allowing you to look up a staker's info.
5. **`IERC20 public token;`**: The address of the ERC20 token that will be staked.
6. **`IERC20 public rewardToken;`**: The address of the ERC20 token used to reward stakers.
7. **`uint256 public totalStaked;`**: Tracks the total amount of tokens staked in the contract.
8. **`struct Tier { ... }`**: Defines tiers for staking:
* `tierId`: A unique ID for this tier.
* `apr`: The annual percentage rate for this tier (expressed as a percentage e.g. 1000 = 10%).
* `minStake`: The minimum stake amount for this tier.
9. **`Tier[] public tiers;`**: A dynamically sized array to hold the tiers.
10. **`uint256 public constant SECONDS_IN_YEAR = 365 days;`**: A constant to represent the number of seconds in a year.
11. **`event Staked( ... );`**: An event emitted when a user stakes tokens.
12. **`event Unstaked( ... );`**: An event emitted when a user unstakes tokens.
13. **`event RewardPaid( ... );`**: An event emitted when a user receives a reward.
14. **`constructor(address _tokenAddress, address _rewardTokenAddress) { ... }`**: The constructor, which sets the addresses of the staking token and reward token.
15. **`addTier(uint8 _tierId, uint256 _apr, uint256 _minStake) external { ... }`**: Adds a new staking tier. Only contract owner can call this function.
16. **`stake(uint256 _amount, uint8 _tierId) external { ... }`**: Allows a user to stake tokens:
* Requires that the user isn't already staking.
* Requires that the amount staked is greater than zero and meets the minimum stake for the tier.
* Transfers tokens from the user to the contract using `token.transferFrom()`. (The user needs to `approve` the contract to spend their tokens first).
* Updates the `stakers` mapping with the stake information.
* Updates `totalStaked`.
* Emits the `Staked` event.
17. **`unstake() external { ... }`**: Allows a user to unstake their tokens and claim rewards:
* Requires that the user is currently staking.
* Calculates the reward using `calculateReward()`.
* Removes the staker's information from the `stakers` mapping.
* Transfers the staked tokens and rewards to the user using `token.transfer()` and `rewardToken.transfer()`.
* Emits `Unstaked` and `RewardPaid` events.
18. **`calculateReward(address _user) public view returns (uint256) { ... }`**: Calculates the reward for a given user:
* Requires that the user is currently staking.
* Calculates the time elapsed since the staking began.
* Calculates the reward based on the staked amount, APR, and time elapsed.
* `reward = (staker.amount * selectedTier.apr * timeElapsed) / (1000 * SECONDS_IN_YEAR);`
* Returns the calculated reward.
19. **`getTier(uint8 _tierId) internal view returns (Tier memory){ ... }`**: Helper function to retrieve a tier by its ID.
20. **`interface IERC20 { ... }`**: An interface for interacting with ERC20 tokens. It defines the essential functions (like `transfer`, `balanceOf`, `approve`, etc.) that your contract needs to call on the staking and reward tokens.
**JavaScript (Simulated UI - console-based):**
```javascript
// Simulated Token Contract (for demonstration purposes)
class MockERC20 {
constructor(name, symbol, initialSupply, decimals = 18) {
this.name = name;
this.symbol = symbol;
this.totalSupply = initialSupply;
this.balances = {};
this.allowances = {};
this.decimals = decimals;
}
balanceOf(account) {
return this.balances[account] || 0;
}
transfer(recipient, amount) {
if (this.balances[msg.sender] >= amount) {
this.balances[msg.sender] -= amount;
this.balances[recipient] = (this.balances[recipient] || 0) + amount;
return true;
}
return false;
}
transferFrom(sender, recipient, amount) {
if (this.allowances[sender][msg.sender] >= amount && this.balances[sender] >= amount) {
this.balances[sender] -= amount;
this.balances[recipient] = (this.balances[recipient] || 0) + amount;
this.allowances[sender][msg.sender] -= amount;
return true;
}
return false;
}
approve(spender, amount) {
this.allowances[msg.sender] = this.allowances[msg.sender] || {};
this.allowances[msg.sender][spender] = amount;
return true;
}
}
// Mock Blockchain and Context
let block = {
timestamp: Math.floor(Date.now() / 1000) // Current Unix timestamp
};
let msg = {
sender: "0xf39Fd6e515471Ac92314Dc00b966FA861295180e" // Example address
};
// Simulated Staking Contract (Interacts with MockERC20)
class StakingContract {
constructor(token, rewardToken) {
this.token = token;
this.rewardToken = rewardToken;
this.stakers = {};
this.totalStaked = 0;
this.tiers = [];
this.SECONDS_IN_YEAR = 365 * 24 * 60 * 60;
}
addTier(tierId, apr, minStake) {
if (this.tiers.length < 5) {
this.tiers.push({ tierId, apr, minStake });
} else {
console.log("Max tiers reached.");
}
}
stake(amount, tierId) {
if (this.stakers[msg.sender]) {
console.log("Already staked.");
return;
}
const selectedTier = this.getTier(tierId);
if (!selectedTier) {
console.log("Invalid tier.");
return;
}
if (amount < selectedTier.minStake) {
console.log("Insufficient amount for selected tier.");
return;
}
if (this.token.transferFrom(msg.sender, this, amount)) {
this.stakers[msg.sender] = {
amount: amount,
startTime: block.timestamp,
tierId: tierId
};
this.totalStaked += amount;
console.log(`${msg.sender} staked ${amount} tokens in tier ${tierId}.`);
} else {
console.log("Stake transfer failed.");
}
}
unstake() {
const staker = this.stakers[msg.sender];
if (!staker) {
console.log("Not staked.");
return;
}
const reward = this.calculateReward(msg.sender);
const amount = staker.amount;
delete this.stakers[msg.sender];
this.totalStaked -= amount;
if (this.token.transfer(msg.sender, amount)) {
if (this.rewardToken.transfer(msg.sender, reward)) {
console.log(`${msg.sender} unstaked ${amount} tokens and received ${reward} reward tokens.`);
} else {
console.log("Reward transfer failed.");
//Revert Logic (In a real system, you'd want proper state reversion)
}
} else {
console.log("Unstake transfer failed.");
//Revert Logic
}
}
calculateReward(user) {
const staker = this.stakers[user];
if (!staker) {
console.log("Not staked.");
return 0;
}
const selectedTier = this.getTier(staker.tierId);
if (!selectedTier) {
console.log("Invalid tier.");
return 0;
}
const timeElapsed = block.timestamp - staker.startTime;
const reward = (staker.amount * selectedTier.apr * timeElapsed) / (1000 * this.SECONDS_IN_YEAR);
return reward;
}
getTier(tierId) {
return this.tiers.find(tier => tier.tierId === tierId);
}
}
// Example Usage
const stakingToken = new MockERC20("StakingToken", "STK", 1000000);
const rewardToken = new MockERC20("RewardToken", "RWD", 100000);
const stakingContract = new StakingContract(stakingToken, rewardToken);
//Initial Token Distribution
stakingToken.balances[msg.sender] = 10000;
// Set up some tiers
stakingContract.addTier(1, 50, 100); // Tier 1: 5% APR, min stake 100
stakingContract.addTier(2, 100, 1000); // Tier 2: 10% APR, min stake 1000
// Simulate staking
stakingToken.approve(stakingContract, 5000); //Approve contract to spend tokens
stakingContract.stake(500, 1); // Stake 500 in Tier 1
console.log("Staking Token Balance: ", stakingToken.balanceOf(msg.sender));
//Advance Time
block.timestamp += 365 * 24 * 60 * 60; //One year later
// Simulate unstaking
stakingContract.unstake();
console.log("Staking Token Balance: ", stakingToken.balanceOf(msg.sender));
console.log("Reward Token Balance: ", rewardToken.balanceOf(msg.sender));
```
**Explanation of JavaScript:**
1. **`MockERC20` class**: Simulates an ERC20 token. It's a simplified version of an ERC20 and includes `balanceOf`, `transfer`, `transferFrom`, and `approve` methods. In a real application, you'd use a library like `ethers.js` or `web3.js` to interact with actual ERC20 contracts on the blockchain.
2. **`block` and `msg` objects**: These are mock objects that simulate the blockchain environment. `block.timestamp` represents the current block timestamp, and `msg.sender` represents the address of the user interacting with the contract.
3. **`StakingContract` class**: This simulates the Solidity staking contract.
* `constructor`: Initializes the contract with the staking token and reward token.
* `addTier`: Adds a staking tier.
* `stake`: Simulates staking tokens. It checks if the user is already staking, verifies the amount, and then transfers the tokens.
* `unstake`: Simulates unstaking tokens and claiming rewards. It calculates the reward, transfers tokens back to the user, and transfers the reward tokens.
* `calculateReward`: Calculates the reward based on the staked amount, APR, and time elapsed.
* `getTier`: retrieves tier based on tierID.
4. **Example Usage**:
* Creates instances of `MockERC20` for the staking token and reward token.
* Creates an instance of the `StakingContract`.
* Sets up some staking tiers using `addTier`.
* Simulates a user staking tokens using `stake`.
* Advances the `block.timestamp` to simulate time passing.
* Simulates a user unstaking tokens using `unstake`.
* Prints the balances of the user to show the results.
**How to Run:**
1. **Solidity (Deployment):**
* You'll need a Solidity development environment like Remix, Truffle, or Hardhat.
* Compile the `MultiTierStaking.sol` contract.
* Deploy the staking token, reward token, and the staking contract to a test network (e.g., Ganache, Hardhat Network, Goerli).
* Note the addresses of the deployed contracts.
2. **JavaScript (Simulation):**
* Copy the JavaScript code into a file (e.g., `staking.js`).
* Open your browser's developer console (usually by pressing F12).
* Copy and paste the JavaScript code into the console and press Enter.
* The script will execute, simulating the staking process and printing results to the console.
**Key Improvements and Considerations for Real-World Deployment:**
* **Use a Proper Blockchain Interaction Library:** Replace the `MockERC20` class with a library like `ethers.js` or `web3.js` to interact with real ERC20 contracts and the Ethereum blockchain.
* **Error Handling:** Add more robust error handling to the JavaScript code.
* **User Interface:** Create a proper HTML/CSS-based user interface for a better user experience.
* **Security Audits:** Before deploying to a live network, have your Solidity contract audited by security professionals.
* **Gas Optimization:** Optimize your Solidity code to minimize gas costs. Consider using techniques like:
* Using `calldata` instead of `memory` for function arguments when appropriate.
* Packing variables to reduce storage costs.
* Using assembly for critical calculations.
* **Upgradability:** Consider using a proxy pattern to make your contract upgradable in the future (using OpenZeppelin's libraries for secure upgradability).
* **Events:** Use events extensively to provide off-chain applications with information about the contract's state.
* **Access Control:** Implement proper access control using `Ownable` from OpenZeppelin to restrict certain functions (like adding tiers) to the contract owner.
* **Time Manipulation:** Be careful when manipulating time in tests or simulations. Use functions like `block.timestamp` carefully and avoid relying on wall-clock time in production code.
This example provides a basic framework. Building a secure and robust staking contract requires a deep understanding of Solidity, blockchain security principles, and best practices. Good luck!
👁️ Viewed: 9
Comments