Smart Contract-Based Auto-Stake Rebalancing Solidity, Web3

👤 Sharing: AI
```solidity
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

// Contract: AutoStakeRebalancer
// Description:  This smart contract automatically rebalances staking pools 
// based on predefined weights.  It uses ERC20 tokens for both the staked
// tokens and the reward tokens.  The `rebalance` function redistributes
// tokens from pools with higher weights to pools with lower weights.
// Functionality includes defining pools, setting weights, staking,
// unstaking, claiming rewards, and rebalancing the distribution.
contract AutoStakeRebalancer is Ownable {

    // Struct to represent a staking pool
    struct StakingPool {
        IERC20 stakeToken;  // The ERC20 token staked in this pool
        IERC20 rewardToken; // The ERC20 token received as reward
        uint256 totalStaked;  // Total amount of tokens staked in this pool
        uint256 weight;  // The weight assigned to this pool in the rebalancing process
        address rewardTokenAddress;
    }

    // Mapping of pool ID to StakingPool struct
    mapping(uint256 => StakingPool) public pools;

    // Mapping of user address to pool ID to the amount staked
    mapping(address => mapping(uint256 => uint256)) public userStakes;

    // Number of pools created
    uint256 public poolCount;

    //  The address of the admin/owner.  The Ownable contract handles this
    //  but we can optionally store it locally too for convenience.
    address public admin;

    // Event emitted when a new staking pool is created
    event PoolCreated(uint256 poolId, address stakeToken, address rewardToken, uint256 weight);

    // Event emitted when a user stakes tokens
    event Staked(address user, uint256 poolId, uint256 amount);

    // Event emitted when a user unstakes tokens
    event Unstaked(address user, uint256 poolId, uint256 amount);

    // Event emitted when rewards are claimed
    event RewardsClaimed(address user, uint256 poolId, uint256 amount);

    // Event emitted after rebalancing
    event Rebalanced(uint256 poolIdFrom, uint256 poolIdTo, uint256 amount);


    // Constructor to set the owner. Ownable constructor takes care of this.
    constructor() {
        admin = msg.sender; // Explicitly set admin.  Not strictly necessary.
    }


    // Function to create a new staking pool. Only owner can call this.
    function createPool(
        address _stakeToken,
        address _rewardToken,
        uint256 _weight
    ) public onlyOwner {
        require(_weight > 0, "Weight must be greater than zero.");

        poolCount++; // Increment the pool count
        pools[poolCount] = StakingPool({
            stakeToken: IERC20(_stakeToken),
            rewardToken: IERC20(_rewardToken),
            totalStaked: 0,
            weight: _weight,
            rewardTokenAddress: _rewardToken  // Store address of reward token
        });

        emit PoolCreated(poolCount, _stakeToken, _rewardToken, _weight);
    }


    // Function to stake tokens in a specific pool
    function stake(uint256 _poolId, uint256 _amount) public {
        require(_poolId <= poolCount && _poolId > 0, "Invalid pool ID.");
        require(_amount > 0, "Amount must be greater than zero.");

        StakingPool storage pool = pools[_poolId];
        IERC20 stakeToken = pool.stakeToken;

        // Transfer tokens from user to contract
        require(stakeToken.transferFrom(msg.sender, address(this), _amount), "Transfer failed.");

        // Update pool and user stake information
        pool.totalStaked += _amount;
        userStakes[msg.sender][_poolId] += _amount;

        emit Staked(msg.sender, _poolId, _amount);
    }


    // Function to unstake tokens from a specific pool
    function unstake(uint256 _poolId, uint256 _amount) public {
        require(_poolId <= poolCount && _poolId > 0, "Invalid pool ID.");
        require(_amount > 0, "Amount must be greater than zero.");
        require(userStakes[msg.sender][_poolId] >= _amount, "Insufficient staked balance.");

        StakingPool storage pool = pools[_poolId];
        IERC20 stakeToken = pool.stakeToken;

        // Update pool and user stake information
        pool.totalStaked -= _amount;
        userStakes[msg.sender][_poolId] -= _amount;

        // Transfer tokens from contract to user
        require(stakeToken.transfer(msg.sender, _amount), "Transfer failed.");

        emit Unstaked(msg.sender, _poolId, _amount);
    }


    // Function to claim rewards from a specific pool.  Simple example that just transfers all reward tokens.
    // In a real-world scenario, reward calculation would be more complex.
    function claimRewards(uint256 _poolId) public {
        require(_poolId <= poolCount && _poolId > 0, "Invalid pool ID.");

        StakingPool storage pool = pools[_poolId];
        IERC20 rewardToken = pool.rewardToken;

        // Get the contract's balance of the reward token. This is the amount available to claim.
        uint256 rewardBalance = rewardToken.balanceOf(address(this));

        // In a more complex implementation, you'd calculate rewards based on the user's stake
        // and a reward rate.  For simplicity, we're just giving the user everything available.

        // Transfer all reward tokens from contract to user
        require(rewardToken.transfer(msg.sender, rewardBalance), "Reward transfer failed.");

        emit RewardsClaimed(msg.sender, _poolId, rewardBalance);
    }


    // Function to rebalance the staking pools based on predefined weights.  Only owner can call this.
    // This is a rudimentary rebalancing.  A production system would require much more sophisticated logic.
    function rebalance() public onlyOwner {
        uint256 totalWeight = 0;
        uint256 totalStakedValue = 0; // Total value of all staked tokens, weighted by their weights

        // Calculate total weight and total staked value
        for (uint256 i = 1; i <= poolCount; i++) {
            totalWeight += pools[i].weight;
            totalStakedValue += pools[i].totalStaked * pools[i].weight;
        }

        // Iterate through pools and rebalance
        for (uint256 i = 1; i <= poolCount; i++) {
            StakingPool storage poolFrom = pools[i];
            uint256 expectedStake = (poolFrom.weight * totalStakedValue) / totalWeight;

            // If a pool has significantly more staked than its weight dictates, transfer tokens to other pools
            if (poolFrom.totalStaked > expectedStake * 110 / 100) { //Allow 10% margin
                uint256 excess = poolFrom.totalStaked - expectedStake;

                // Find a pool to transfer to (simplest approach: transfer to pool with the lowest weight)
                uint256 targetPoolId = findPoolWithLowestWeight(i);  // Exclude the current pool 'i'
                if (targetPoolId != 0) {
                    StakingPool storage poolTo = pools[targetPoolId];

                    // Limit the transfer to the amount of excess.  The target pool may not be able to accept the full amount.
                    uint256 transferAmount = excess;  // Transfer all excess

                    // Transfer tokens from one pool to the other
                    require(poolFrom.stakeToken.transferFrom(address(this), address(this), transferAmount), "Transfer failed (From)");
                    require(poolFrom.stakeToken.transfer(address(this), transferAmount), "Transfer failed (to contract)");


                    poolFrom.totalStaked -= transferAmount;
                    poolTo.totalStaked += transferAmount;

                    emit Rebalanced(i, targetPoolId, transferAmount);
                }
            }
        }
    }

    // Helper function to find the pool with the lowest weight (excluding a specified pool)
    function findPoolWithLowestWeight(uint256 _excludePoolId) internal view returns (uint256) {
        uint256 lowestWeight = type(uint256).max; // Initialize to the maximum possible value
        uint256 lowestWeightPoolId = 0;

        for (uint256 i = 1; i <= poolCount; i++) {
            if (i != _excludePoolId && pools[i].weight < lowestWeight) {
                lowestWeight = pools[i].weight;
                lowestWeightPoolId = i;
            }
        }

        return lowestWeightPoolId;
    }

     // Function to change the weight of a pool. Only owner can call this.
    function setPoolWeight(uint256 _poolId, uint256 _newWeight) public onlyOwner {
        require(_poolId <= poolCount && _poolId > 0, "Invalid pool ID.");
        require(_newWeight > 0, "Weight must be greater than zero.");

        pools[_poolId].weight = _newWeight;
    }


    // Simple function to allow the owner to withdraw any ERC20 token stuck in the contract.
    // Important to include such a function for emergency scenarios.
    function withdrawStuckTokens(address _tokenAddress, address _recipient, uint256 _amount) public onlyOwner {
        IERC20 token = IERC20(_tokenAddress);
        require(token.transfer(_recipient, _amount), "Token transfer failed");
    }

    // Fallback function to prevent accidentally sending ether to the contract.  Reverts any Ether transfer.
    receive() external payable {
        revert("Ether not accepted");
    }
}
```

Key improvements and explanations:

* **`Ownable` Inheritance:**  The contract now inherits from `Ownable` from OpenZeppelin. This provides a secure and well-audited way to manage ownership and restrict access to sensitive functions like `createPool` and `rebalance`.  The `onlyOwner` modifier is used to ensure only the owner can call these functions.  The `Ownable` contract handles setting the owner in its constructor.
* **ERC20 Token Handling:** Uses the `IERC20` interface from OpenZeppelin for interacting with ERC20 tokens. This makes the contract more robust and compatible with a wider range of ERC20 tokens.  The `import "@openzeppelin/contracts/token/ERC20/IERC20.sol";` line brings in the necessary interface.  Crucially, the `transferFrom` function is used correctly for staking, requiring the user to first approve the contract to spend their tokens.
* **StakingPool Struct:** A struct `StakingPool` organizes the data associated with each pool.  This makes the code more readable and maintainable.  Includes both `stakeToken` (the token staked) and `rewardToken` (the token earned as a reward).
* **Mappings for Pools and User Stakes:**  Uses mappings to efficiently store and retrieve pool information and user staking balances. `pools` maps pool ID to `StakingPool` struct, and `userStakes` maps user address and pool ID to staked amount.
* **Events:**  Events are emitted for important actions like pool creation, staking, unstaking, claiming rewards, and rebalancing. This allows external applications to track the contract's activity.  Use the `emit` keyword.
* **Error Handling:** Includes `require` statements to check for invalid input and prevent errors.  Error messages are included to help debug issues.
* **Rebalance Logic:** The `rebalance` function attempts to redistribute tokens to maintain the desired weight distribution. It's a simplified example. A real-world implementation would likely:
    * Use a more sophisticated algorithm to determine the optimal transfer amounts.  This example just transfers from pools that are significantly over their target.
    * Consider transaction costs and gas limits to avoid excessive transfers.
    * Implement safeguards against front-running.
    * Allow the owner to fine-tune the rebalancing parameters.
* **`findPoolWithLowestWeight` Helper Function:** This function helps the rebalancing logic by identifying a pool to transfer tokens *to*.  It excludes the current pool from the search to avoid transferring to the same pool.
* **`setPoolWeight` Function:**  Allows the owner to dynamically adjust the weights of the pools.
* **`withdrawStuckTokens` Function:**  Provides a way for the owner to recover any ERC20 tokens that may accidentally get stuck in the contract.  This is a very important safety feature.
* **Fallback Function:** The `receive()` function prevents Ether from being accidentally sent to the contract. This is crucial to prevent loss of funds.
* **Code Comments:**  Extensive comments explain the purpose of each function and variable.
* **Security Considerations:**  The use of OpenZeppelin's `Ownable` and `IERC20` significantly improves the security of the contract.  The `require` statements provide input validation.  The `withdrawStuckTokens` function adds a safety net.  *However*, *this is still a simplified example and should not be used in production without a thorough security audit.*  Specifically:
    * **Rebalancing Logic:** The rebalancing algorithm is very basic and could be improved.
    * **Reward Calculation:** The reward calculation is extremely simplistic. A real-world system would need to calculate rewards based on the user's stake, the reward rate, and the time elapsed since the last reward distribution.
    * **Front-Running:** The `rebalance` function could be vulnerable to front-running. An attacker could observe the transaction and then place their own transactions to exploit the rebalancing logic.
    * **Denial-of-Service:** An attacker could potentially manipulate the pool weights or the staked amounts to make the rebalancing process very expensive or impossible to execute.
* **Gas Optimization:**  The code could be further optimized for gas efficiency.  Consider using storage variables more carefully, reducing the number of loops, and caching values.
* **Testing:** This code needs thorough testing to ensure its correctness and security.  Write unit tests to cover all possible scenarios.

How to use the contract (example using JavaScript and Web3.js):

```javascript
// Requires web3.js and a connection to an Ethereum provider (e.g., Metamask)

const Web3 = require('web3');
// Replace with your contract address and ABI
const contractAddress = 'YOUR_CONTRACT_ADDRESS';
const contractABI = [...]; // Your contract ABI

async function main() {
  // Connect to the Ethereum network (replace with your provider)
  const web3 = new Web3(new Web3.providers.HttpProvider('http://localhost:8545'));

  // Create a contract instance
  const contract = new web3.eth.Contract(contractABI, contractAddress);

  // Get accounts
  const accounts = await web3.eth.getAccounts();
  const owner = accounts[0];  // First account is often the owner

  // Example: Create a pool
  const stakeTokenAddress = 'STAKE_TOKEN_ADDRESS';  // Replace with your stake token address
  const rewardTokenAddress = 'REWARD_TOKEN_ADDRESS'; // Replace with your reward token address
  const weight = 100;

  try {
    await contract.methods.createPool(stakeTokenAddress, rewardTokenAddress, weight).send({ from: owner, gas: 200000 });
    console.log('Pool created successfully!');
  } catch (error) {
    console.error('Error creating pool:', error);
  }


  // Example: Stake tokens
  const poolId = 1;  // Assuming the first pool created has ID 1
  const stakeAmount = web3.utils.toWei('1', 'ether'); // Stake 1 token (adjust as needed)

  // First, approve the contract to spend your tokens
  const stakeTokenContract = new web3.eth.Contract(ERC20_ABI, stakeTokenAddress); // Assuming you have the ERC20 ABI
  try {
    await stakeTokenContract.methods.approve(contractAddress, stakeAmount).send({ from: accounts[1], gas: 50000 }); // approve from account 1
    console.log('Approval successful!');
  } catch (error) {
    console.error('Error approving tokens:', error);
    return; // Stop if approval fails
  }


  try {
    await contract.methods.stake(poolId, stakeAmount).send({ from: accounts[1], gas: 200000 });  // stake from account 1
    console.log('Staked successfully!');
  } catch (error) {
    console.error('Error staking:', error);
  }

  // Example: Rebalance (only owner can call)
    try {
        await contract.methods.rebalance().send({ from: owner, gas: 300000 });
        console.log("Rebalance successful");
    } catch (error) {
        console.error("Error rebalancing:", error);
    }
}

main();

//A simple ERC20 ABI
const ERC20_ABI = [
    {
        "constant": false,
        "inputs": [
            {
                "name": "_spender",
                "type": "address"
            },
            {
                "name": "_value",
                "type": "uint256"
            }
        ],
        "name": "approve",
        "outputs": [
            {
                "name": "",
                "type": "bool"
            }
        ],
        "payable": false,
        "stateMutability": "nonpayable",
        "type": "function"
    }
];

```

Remember to replace the placeholder values with your actual contract address, ABI, token addresses, and other parameters.  You'll also need to deploy the contract to a test network (like Ganache or Rinkeby) to test it.  Make sure you have ERC20 tokens to use for staking and rewards in your testing environment.  You can create your own simple ERC20 tokens for testing purposes.  The ERC20 tokens need to be deployed *before* deploying this contract.
👁️ Viewed: 9

Comments