Decentralized Automated Yield Farming Solidity, JavaScript, Web3

👤 Sharing: AI
Okay, here's a simplified example of a decentralized automated yield farming program using Solidity, JavaScript, and Web3.  This is a basic illustration and will need significant expansion for a production environment.  It focuses on the core concepts.

**Important Considerations:**

*   **Security:**  This is a simplified example and *not* production-ready.  Yield farming contracts can be incredibly complex and require rigorous auditing.  Vulnerabilities can lead to significant financial losses.
*   **Gas Optimization:**  Solidity code must be written with gas optimization in mind.  Inefficient code can be extremely costly to execute on the Ethereum network.
*   **External Dependencies:**  Real-world yield farms often interact with other DeFi protocols (e.g., Uniswap, Compound, Aave). This example simplifies that interaction for clarity.
*   **Error Handling:**  Proper error handling is crucial to prevent unexpected behavior.  This example includes some basic checks but would need more robust error handling in a real-world scenario.

**1. Solidity Contract (YieldFarm.sol):**

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

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

contract YieldFarm is Ownable {

    IERC20 public rewardToken;  // The token users earn as rewards.
    IERC20 public stakingToken; // The token users stake.

    uint256 public rewardRate;   // Rewards distributed per second (e.g., in Wei).
    uint256 public lastUpdateTime; // Last time rewards were updated.
    uint256 public duration = 7 days;  // Duration of the yield farm

    uint256 public totalStaked; // Total amount of stakingToken staked in the contract.

    mapping(address => uint256) public stakedBalances;  // How much each user has staked.
    mapping(address => uint256) public earnedRewards; // How much each user has earned but not claimed.

    event Staked(address indexed user, uint256 amount);
    event Unstaked(address indexed user, uint256 amount);
    event RewardPaid(address indexed user, uint256 reward);

    constructor(
        address _rewardToken,
        address _stakingToken,
        uint256 _rewardRate
    ) Ownable(msg.sender) {
        rewardToken = IERC20(_rewardToken);
        stakingToken = IERC20(_stakingToken);
        rewardRate = _rewardRate;
        lastUpdateTime = block.timestamp;
    }

    modifier updateReward(address account) {
        rewardPerToken();
        earned(account);
        _;
    }

    function rewardPerToken() internal {
        if (totalStaked == 0) {
            lastUpdateTime = block.timestamp; //prevent divide by zero error
            return;
        }
        uint256 timeElapsed = block.timestamp - lastUpdateTime;
        if (timeElapsed > duration) {
            timeElapsed = duration;  // Cap the time elapsed
        }
        uint256 reward = timeElapsed * rewardRate;
        rewardToken.transfer(address(this), reward); //transfer reward tokens to contract
        lastUpdateTime = block.timestamp;
    }

    function earned(address account) internal {
        earnedRewards[account] = calculateReward(account);
    }

    function calculateReward(address account) public view returns (uint256) {
        uint256 timeElapsed = block.timestamp - lastUpdateTime;
        if (timeElapsed > duration) {
            timeElapsed = duration;  // Cap the time elapsed
        }
        uint256 potentialReward = timeElapsed * rewardRate;

        uint256 balance = stakedBalances[account];
        return (balance * potentialReward) / totalStaked;
    }


    function stake(uint256 amount) external updateReward(msg.sender) {
        require(amount > 0, "Cannot stake 0");
        require(stakingToken.transferFrom(msg.sender, address(this), amount), "Transfer failed");

        stakedBalances[msg.sender] += amount;
        totalStaked += amount;

        emit Staked(msg.sender, amount);
    }

    function unstake(uint256 amount) external updateReward(msg.sender) {
        require(amount > 0, "Cannot unstake 0");
        require(stakedBalances[msg.sender] >= amount, "Insufficient staked balance");

        stakedBalances[msg.sender] -= amount;
        totalStaked -= amount;

        require(stakingToken.transfer(msg.sender, amount), "Transfer failed"); // send staked tokens to user

        emit Unstaked(msg.sender, amount);
    }

    function claimRewards() external updateReward(msg.sender) {
        uint256 reward = earnedRewards[msg.sender];
        require(reward > 0, "No rewards to claim");

        earnedRewards[msg.sender] = 0; // Reset reward for this user.

        require(rewardToken.transfer(msg.sender, reward), "Reward transfer failed");  // send reward tokens to user

        emit RewardPaid(msg.sender, reward);
    }


    // Emergency function to withdraw any stuck tokens (only callable by the owner).
    function emergencyWithdraw(address tokenAddress, address to, uint256 amount) external onlyOwner {
        IERC20(tokenAddress).transfer(to, amount);
    }

    //setters
    function setRewardRate(uint256 _rewardRate) external onlyOwner {
        rewardRate = _rewardRate;
    }

    function setDuration(uint256 _duration) external onlyOwner {
        duration = _duration;
    }

}
```

**Explanation of Solidity Contract:**

*   **`pragma solidity ^0.8.0;`**: Specifies the Solidity compiler version.
*   **`import "@openzeppelin/contracts/token/ERC20/IERC20.sol";`**: Imports the IERC20 interface from OpenZeppelin for interacting with ERC20 tokens.
*   **`import "@openzeppelin/contracts/access/Ownable.sol";`**: Imports the Ownable contract from OpenZeppelin to implement an owner-controlled contract (only the owner can call certain functions).
*   **`contract YieldFarm`**: Defines the contract named `YieldFarm`.
*   **State Variables:**
    *   `rewardToken`:  An `IERC20` interface representing the ERC20 token distributed as rewards.
    *   `stakingToken`: An `IERC20` interface representing the ERC20 token users stake in the contract.
    *   `rewardRate`: The amount of `rewardToken` distributed per second.
    *   `lastUpdateTime`: The last time rewards were calculated and distributed.
    *   `duration`: The duration of the yield farm, in seconds
    *   `totalStaked`: The total amount of `stakingToken` currently staked in the contract.
    *   `stakedBalances`: A mapping from user address to the amount of `stakingToken` they have staked.
    *   `earnedRewards`: A mapping from user address to the amount of `rewardToken` they have earned but not yet claimed.
*   **Events:** Events are emitted when certain actions occur, allowing external applications to track the contract's state.
    *   `Staked`: Emitted when a user stakes tokens.
    *   `Unstaked`: Emitted when a user unstakes tokens.
    *   `RewardPaid`: Emitted when a user claims their rewards.
*   **`constructor()`**:  Initializes the contract with the addresses of the `rewardToken` and `stakingToken`, and the initial `rewardRate`.
*   **`modifier updateReward(address account)`**: A modifier that updates the rewards for a given account before executing the function it modifies.
*   **`rewardPerToken()`**: Calculates and distributes rewards based on the time elapsed since the last update and the total amount staked. It also transfers rewards from the account to the contract.
*   **`earned(address account)`**: Calculates the amount of rewards earned by a given account and stores it in the `earnedRewards` mapping.
*   **`calculateReward(address account)`**: Calculates the amount of rewards a user is entitled to based on their staked balance and the time elapsed since the last update.
*   **`stake(uint256 amount)`**: Allows a user to stake `amount` of `stakingToken`.  It requires the user to approve the contract to spend their `stakingToken` before calling this function.  It also updates reward calculations before staking.
*   **`unstake(uint256 amount)`**: Allows a user to unstake `amount` of `stakingToken`.  It also updates reward calculations before unstaking.
*   **`claimRewards()`**:  Allows a user to claim their accumulated `rewardToken`.  It resets the user's `earnedRewards` to 0 after the claim.
*   **`emergencyWithdraw()`**:  Allows the owner to withdraw any ERC20 tokens accidentally sent to the contract (for emergency situations).
*   **`setRewardRate(uint256 _rewardRate)`**: Allows the owner to set the reward rate.
*   **`setDuration(uint256 _duration)`**: Allows the owner to set the duration of the yield farm.

**2. JavaScript (Web3) Interaction (index.js):**

```javascript
const Web3 = require('web3');
const YieldFarmABI = require('./YieldFarm.json').abi; //ABI of the smart contract
const RewardTokenABI = require('./IERC20.json').abi; //ABI of the reward token smart contract
const StakingTokenABI = require('./IERC20.json').abi; //ABI of the staking token smart contract

// Set provider (e.g., Ganache, Infura, or MetaMask)
const web3 = new Web3('http://localhost:8545'); // Example: Ganache

// Contract Addresses (Replace with your actual deployed contract addresses)
const yieldFarmAddress = 'YOUR_YIELDFARM_CONTRACT_ADDRESS';
const rewardTokenAddress = 'YOUR_REWARDTOKEN_CONTRACT_ADDRESS';
const stakingTokenAddress = 'YOUR_STAKINGTOKEN_CONTRACT_ADDRESS';

// Contract Instances
const yieldFarm = new web3.eth.Contract(YieldFarmABI, yieldFarmAddress);
const rewardToken = new web3.eth.Contract(RewardTokenABI, rewardTokenAddress);
const stakingToken = new web3.eth.Contract(StakingTokenABI, stakingTokenAddress);

// Account (Replace with your actual account address and private key for testing)
const accountAddress = 'YOUR_ACCOUNT_ADDRESS';
const privateKey = 'YOUR_PRIVATE_KEY';

// Function to sign and send transactions
async function signAndSendTransaction(txObject) {
    const gas = await txObject.estimateGas({ from: accountAddress });
    const gasPrice = await web3.eth.getGasPrice(); //Use web3.eth.getGasPrice() to get the current gas price in wei
    const nonce = await web3.eth.getTransactionCount(accountAddress);

    const txData = {
        to: txObject._parent._address,
        data: txObject.encodeABI(),
        gas: gas,
        gasPrice: gasPrice,
        nonce: nonce,
    };

    const signedTx = await web3.eth.accounts.signTransaction(txData, privateKey);
    const receipt = await web3.eth.sendSignedTransaction(signedTx.rawTransaction);

    console.log(`Transaction Hash: ${receipt.transactionHash}`);
    return receipt;
}


async function stakeTokens(amount) {
    try {
        // Approve the YieldFarm contract to spend the staking tokens on behalf of the user
        const approveTx = stakingToken.methods.approve(yieldFarmAddress, amount);
        await signAndSendTransaction(approveTx);
        console.log('Approval transaction successful');

        // Stake tokens
        const stakeTx = yieldFarm.methods.stake(amount);
        await signAndSendTransaction(stakeTx);
        console.log('Stake transaction successful');
    } catch (error) {
        console.error('Error staking tokens:', error);
    }
}

async function unstakeTokens(amount) {
    try {
        const unstakeTx = yieldFarm.methods.unstake(amount);
        await signAndSendTransaction(unstakeTx);
        console.log('Unstake transaction successful');
    } catch (error) {
        console.error('Error unstaking tokens:', error);
    }
}

async function claimRewards() {
    try {
        const claimTx = yieldFarm.methods.claimRewards();
        await signAndSendTransaction(claimTx);
        console.log('Claim rewards transaction successful');
    } catch (error) {
        console.error('Error claiming rewards:', error);
    }
}

async function checkBalances() {
    try {
        const stakedBalance = await yieldFarm.methods.stakedBalances(accountAddress).call();
        const earnedReward = await yieldFarm.methods.earnedRewards(accountAddress).call();
        const rewardBalance = await rewardToken.methods.balanceOf(accountAddress).call();
        const stakingBalance = await stakingToken.methods.balanceOf(accountAddress).call();
        console.log(`Staked Balance: ${web3.utils.fromWei(stakedBalance, 'ether')}`);
        console.log(`Earned Rewards: ${web3.utils.fromWei(earnedReward, 'ether')}`);
        console.log(`Reward Token Balance: ${web3.utils.fromWei(rewardBalance, 'ether')}`);
        console.log(`Staking Token Balance: ${web3.utils.fromWei(stakingBalance, 'ether')}`);
    } catch (error) {
        console.error('Error checking balances:', error);
    }
}

async function main() {
    // Example usage
    const stakeAmount = web3.utils.toWei('1', 'ether'); // Stake 1 staking token

    await checkBalances(); //Check balances before
    await stakeTokens(stakeAmount);
    await checkBalances(); //Check balances after staking
    await claimRewards();
    await checkBalances(); //Check balances after claiming rewards
    await unstakeTokens(stakeAmount);
    await checkBalances(); //Check balances after unstaking
}

main();
```

**Explanation of JavaScript Code:**

*   **`const Web3 = require('web3');`**: Imports the Web3 library.
*   **`const YieldFarmABI = require('./YieldFarm.json').abi;`**:  Loads the ABI (Application Binary Interface) of the `YieldFarm` contract from a JSON file (generated during Solidity compilation).
*   **`const web3 = new Web3('http://localhost:8545');`**: Creates a Web3 instance, connecting to a local Ethereum node (e.g., Ganache).  *Change this to your desired provider.*
*   **Contract Addresses:**  *Crucially, you must replace the placeholder contract addresses with the actual addresses where you deployed your contracts.*
*   **Contract Instances:** Creates JavaScript objects representing the deployed contracts, using the ABI and contract address.
*   **Account:** *Replace the placeholder account address and private key with your own.*  Never hardcode private keys in production code! Use secure key management practices.
*   **`signAndSendTransaction(txObject)`**: Signs the transaction with the private key and sends the transaction to the blockchain.
*   **`stakeTokens(amount)`**:
    *   Approves the `YieldFarm` contract to spend `amount` of `stakingToken` on behalf of the user.  This is a necessary step before staking, as the contract needs permission to move the user's tokens.
    *   Calls the `stake()` function on the `YieldFarm` contract to stake the specified amount.
*   **`unstakeTokens(amount)`**: Calls the `unstake()` function on the `YieldFarm` contract to unstake the specified amount.
*   **`claimRewards()`**: Calls the `claimRewards()` function on the `YieldFarm` contract to claim any accrued rewards.
*   **`checkBalances()`**: Retrieves and prints the user's staked balance, earned rewards, reward token balance, and staking token balance.
*   **`main()`**:  An example function that demonstrates how to use the other functions to stake, claim rewards, and unstake.

**3.  Generate ABI files**

*   Compile the Solidity contract using `solc` (Solidity compiler).  You can use Remix IDE or a command-line tool.
*   After compiling, you'll get a `.abi` file (Application Binary Interface) and a `.bin` file (bytecode). The ABI is a JSON file that describes the contract's functions, inputs, and outputs.  The bytecode is the compiled contract code.  You will need the ABI.  Rename the automatically generated ABI for the Reward Token and Staking Token smart contracts to `IERC20.json`
*   In the javascript code, be sure to set the correct contract address.

**4. IERC20.sol**
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

/**
 * @dev Interface of the ERC20 standard as defined in the EIP.
 */
interface IERC20 {
    /**
     * @dev Emitted when `value` tokens are moved from one account (`from`) to
     * another (`to`).
     *
     * Note that `value` may be zero.
     */
    event Transfer(address indexed from, address indexed to, uint256 value);

    /**
     * @dev Emitted when the allowance of a `spender` for an `owner` is set by
     * a call to {approve}. `value` is the new allowance.
     */
    event Approval(address indexed owner, address indexed spender, uint256 value);

    /**
     * @dev Returns the amount of tokens in existence.
     */
    function totalSupply() external view returns (uint256);

    /**
     * @dev Returns the amount of tokens owned by `account`.
     */
    function balanceOf(address account) external view returns (uint256);

    /**
     * @dev Moves `amount` tokens from the caller's account to `to`.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * Emits a {Transfer} event.
     */
    function transfer(address to, uint256 amount) external returns (bool);

    /**
     * @dev Returns the remaining number of tokens that `spender` will be
     * allowed to spend on behalf of `owner` through {transferFrom}. This is
     * zero by default.
     *
     * This value changes when {approve} or {transferFrom} are called.
     */
    function allowance(address owner, address spender) external view returns (uint256);

    /**
     * @dev Sets `amount` as the allowance of `spender` over the caller's tokens.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * IMPORTANT: Beware that changing an allowance with this method brings the risk
     * that someone may use both the old and the new allowance by unfortunate
     * transaction ordering. One possible solution to mitigate this race
     * condition is to first reduce the spender's allowance to 0 and set the
     * desired value afterwards:
     * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
     *
     * Emits an {Approval} event.
     */
    function approve(address spender, uint256 amount) external returns (bool);

    /**
     * @dev Moves `amount` tokens from `from` to `to` using the
     * allowance mechanism. `amount` is then deducted from the caller's
     * allowance.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * Emits a {Transfer} event.
     */
    function transferFrom(
        address from,
        address to,
        uint256 amount
    ) external returns (bool);
}
```

**5.  Setup and Deployment:**

1.  **Install Dependencies:**
    ```bash
    npm install web3 @openzeppelin/contracts
    ```

2.  **Deploy the Contracts:** Deploy the `YieldFarm.sol`, the Reward Token and the Staking Token contracts to a test network (e.g., Ganache) or a real Ethereum network using Remix IDE, Hardhat, or Truffle. Make sure to fund the account you are using to deploy with enough ETH to pay for the gas fees.

3.  **Mint Tokens:** Mint an initial supply of both `rewardToken` and `stakingToken`.  Transfer some `stakingToken` to the account you'll use for testing.  Transfer the reward tokens to the YieldFarm contract.

4.  **Update Addresses:**  In `index.js`, replace the placeholder contract addresses and account address with the actual values from your deployment.

5.  **Run the Script:**
    ```bash
    node index.js
    ```

**Important Notes:**

*   **Error Handling:** The JavaScript code includes basic `try...catch` blocks, but in a real application, you'd want more detailed error handling and logging.
*   **Gas Limit:** You might need to adjust the `gas` limit in the `signAndSendTransaction` function depending on the complexity of the transactions.
*   **Security:** This is a simplified example.  *Do not use this code in production without a thorough security audit.*
*   **User Interface:** A real-world yield farm would have a user interface (built with React or similar) that allows users to easily interact with the contracts.

This comprehensive example gives you a starting point for building your own decentralized automated yield farming program. Remember to prioritize security and gas optimization in a production environment.
👁️ Viewed: 9

Comments