Dynamic APY Adjuster for Staking Solidity, JavaScript

👤 Sharing: AI
Okay, let's create a simplified example of a dynamic APY (Annual Percentage Yield) adjuster for a staking contract using Solidity for the smart contract and JavaScript for a rudimentary frontend to interact with it.  This will be a conceptual illustration.

**Solidity Contract (DynamicAPY.sol)**

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

contract DynamicAPY {

    uint256 public totalStaked;
    uint256 public baseAPY = 10; // Base APY (10%) in percentage form
    uint256 public maxAPY = 25; // Maximum APY (25%)
    uint256 public minAPY = 5;  // Minimum APY (5%)
    uint256 public targetStaked = 10000; // Target Total Stake to influence APY
    uint256 public lastUpdated;

    mapping(address => uint256) public stakedBalances;

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

    constructor() {
        lastUpdated = block.timestamp;
    }

    function stake(uint256 _amount) public {
        require(_amount > 0, "Amount must be greater than zero.");
        stakedBalances[msg.sender] += _amount;
        totalStaked += _amount;
        emit Staked(msg.sender, _amount);
        lastUpdated = block.timestamp;  // Update timestamp on stake
    }

    function unstake(uint256 _amount) public {
        require(_amount > 0, "Amount must be greater than zero.");
        require(stakedBalances[msg.sender] >= _amount, "Insufficient balance.");

        stakedBalances[msg.sender] -= _amount;
        totalStaked -= _amount;
        payable(msg.sender).transfer(_amount);
        emit Unstaked(msg.sender, _amount);
        lastUpdated = block.timestamp;  // Update timestamp on unstake
    }

    function calculateAPY() public view returns (uint256) {
        // APY is adjusted based on how close totalStaked is to targetStaked
        uint256 apy;

        if (totalStaked > targetStaked) {
            //  If total staked exceeds the target, reduce the APY
            apy = baseAPY - (baseAPY - minAPY) * (totalStaked - targetStaked) / targetStaked;

            //Ensure it doesnt go lower than the minimum APY
            if(apy < minAPY){
                apy = minAPY;
            }

        } else {
            //  If total staked is less than the target, increase the APY
            apy = baseAPY + (maxAPY - baseAPY) * (targetStaked - totalStaked) / targetStaked;

             //Ensure it doesnt go above the maximum APY
            if(apy > maxAPY){
                apy = maxAPY;
            }
        }
        return apy;
    }

    // Function to get the current stake of a user
    function getStake(address _user) public view returns (uint256) {
        return stakedBalances[_user];
    }

    // Function to update the targetStaked (only callable by owner in a real application)
    function setTargetStaked(uint256 _newTarget) public {
        targetStaked = _newTarget;
    }

    // Function to update the baseAPY (only callable by owner in a real application)
    function setBaseAPY(uint256 _newAPY) public {
        baseAPY = _newAPY;
    }

    // Function to update the maxAPY (only callable by owner in a real application)
    function setMaxAPY(uint256 _newAPY) public {
        maxAPY = _newAPY;
    }

    // Function to update the minAPY (only callable by owner in a real application)
    function setMinAPY(uint256 _newAPY) public {
        minAPY = _newAPY;
    }
}
```

**Explanation of Solidity Code:**

1.  **`pragma solidity ^0.8.0;`**: Specifies the Solidity compiler version.
2.  **`contract DynamicAPY { ... }`**: Defines the smart contract named `DynamicAPY`.
3.  **State Variables:**
    *   `totalStaked`:  Total amount of tokens staked in the contract.
    *   `baseAPY`:  The baseline Annual Percentage Yield (APY) when the target stake is met. Expressed as a percentage (e.g., 10 for 10%).
    *   `maxAPY`: The maximum allowed APY.
    *   `minAPY`: The minimum allowed APY.
    *   `targetStaked`: The target amount of total staked tokens that influences APY adjustments.  When `totalStaked` is near `targetStaked`, the APY will be closer to the `baseAPY`.
    *   `stakedBalances`: A mapping (dictionary) that stores the staked balance for each user (address).
4.  **Events:**
    *   `Staked`:  Emitted when a user stakes tokens.
    *   `Unstaked`: Emitted when a user unstakes tokens.
5.  **`constructor()`**: Initializes the `lastUpdated` timestamp.
6.  **`stake(uint256 _amount)`**:
    *   Allows users to stake tokens.
    *   Increases the user's `stakedBalances` and `totalStaked`.
    *   Emits the `Staked` event.
    *   Updates the `lastUpdated` timestamp.
7.  **`unstake(uint256 _amount)`**:
    *   Allows users to unstake tokens.
    *   Decreases the user's `stakedBalances` and `totalStaked`.
    *   Transfers the tokens to the user.
    *   Emits the `Unstaked` event.
    *    Updates the `lastUpdated` timestamp.
8.  **`calculateAPY()`**:
    *   Calculates the current APY based on `totalStaked` relative to `targetStaked`.
    *   If `totalStaked` is greater than `targetStaked`, the APY decreases from `baseAPY` toward `minAPY`.
    *   If `totalStaked` is less than `targetStaked`, the APY increases from `baseAPY` toward `maxAPY`.
    *   Uses a linear interpolation formula to determine the APY adjustment.
9.  **`getStake(address _user)`**:  Returns the staked balance of a given user.
10. **`setTargetStaked(uint256 _newTarget)`**, **`setBaseAPY(uint256 _newAPY)`**, **`setMaxAPY(uint256 _newAPY)`**, **`setMinAPY(uint256 _newAPY)`**: Functions to update the target, base, max, and min APY values. In a real-world contract, these should be restricted to an owner or admin role using `onlyOwner` modifiers for security.

**JavaScript Frontend (index.html and app.js)**

This is a very basic frontend to demonstrate interaction.  You'll need to install `ethers` or `web3.js` for this to work.  I'll use `ethers`.

**index.html**

```html
<!DOCTYPE html>
<html>
<head>
    <title>Dynamic APY Staking</title>
    <script src="https://cdn.ethers.io/lib/ethers-5.6.umd.min.js" type="application/javascript"></script>
    <script src="app.js"></script>
</head>
<body>
    <h1>Dynamic APY Staking</h1>

    <label for="stakeAmount">Stake Amount:</label>
    <input type="number" id="stakeAmount">
    <button onclick="stake()">Stake</button><br><br>

    <label for="unstakeAmount">Unstake Amount:</label>
    <input type="number" id="unstakeAmount">
    <button onclick="unstake()">Unstake</button><br><br>

    <button onclick="getAPY()">Get Current APY</button>
    <p id="apyDisplay">APY: </p>

    <button onclick="getStake()">Get My Stake</button>
    <p id="stakeDisplay">My Stake: </p>

    <br>
    <label for="targetStaked">New Target Staked Amount</label>
    <input type="number" id="targetStakedAmount">
    <button onclick="setTarget()">Set Target Stake (Owner Only)</button><br><br>

    <label for="baseAPY">New Base APY Amount</label>
    <input type="number" id="baseAPYAmount">
    <button onclick="setBase()">Set Base APY (Owner Only)</button><br><br>

    <label for="maxAPY">New Max APY Amount</label>
    <input type="number" id="maxAPYAmount">
    <button onclick="setMax()">Set Max APY (Owner Only)</button><br><br>

    <label for="minAPY">New Min APY Amount</label>
    <input type="number" id="minAPYAmount">
    <button onclick="setMin()">Set Min APY (Owner Only)</button><br><br>

</body>
</html>
```

**app.js**

```javascript
const contractAddress = "YOUR_CONTRACT_ADDRESS"; // Replace with your deployed contract address
const contractABI = [
    {
      "inputs": [],
      "stateMutability": "nonpayable",
      "type": "constructor"
    },
    {
      "anonymous": false,
      "inputs": [
        {
          "indexed": true,
          "internalType": "address",
          "name": "user",
          "type": "address"
        },
        {
          "indexed": false,
          "internalType": "uint256",
          "name": "amount",
          "type": "uint256"
        }
      ],
      "name": "Staked",
      "type": "event"
    },
    {
      "anonymous": false,
      "inputs": [
        {
          "indexed": true,
          "internalType": "address",
          "name": "user",
          "type": "address"
        },
        {
          "indexed": false,
          "internalType": "uint256",
          "name": "amount",
          "type": "uint256"
        }
      ],
      "name": "Unstaked",
      "type": "event"
    },
    {
      "inputs": [],
      "name": "baseAPY",
      "outputs": [
        {
          "internalType": "uint256",
          "name": "",
          "type": "uint256"
        }
      ],
      "stateMutability": "view",
      "type": "function"
    },
    {
      "inputs": [],
      "name": "calculateAPY",
      "outputs": [
        {
          "internalType": "uint256",
          "name": "",
          "type": "uint256"
        }
      ],
      "stateMutability": "view",
      "type": "function"
    },
    {
      "inputs": [
        {
          "internalType": "address",
          "name": "_user",
          "type": "address"
        }
      ],
      "name": "getStake",
      "outputs": [
        {
          "internalType": "uint256",
          "name": "",
          "type": "uint256"
        }
      ],
      "stateMutability": "view",
      "type": "function"
    },
    {
      "inputs": [],
      "name": "lastUpdated",
      "outputs": [
        {
          "internalType": "uint256",
          "name": "",
          "type": "uint256"
        }
      ],
      "stateMutability": "view",
      "type": "function"
    },
    {
      "inputs": [],
      "name": "maxAPY",
      "outputs": [
        {
          "internalType": "uint256",
          "name": "",
          "type": "uint256"
        }
      ],
      "stateMutability": "view",
      "type": "function"
    },
    {
      "inputs": [],
      "name": "minAPY",
      "outputs": [
        {
          "internalType": "uint256",
          "name": "",
          "type": "uint256"
        }
      ],
      "stateMutability": "view",
      "type": "function"
    },
    {
      "inputs": [
        {
          "internalType": "uint256",
          "name": "_newAPY",
          "type": "uint256"
        }
      ],
      "name": "setBaseAPY",
      "outputs": [],
      "stateMutability": "nonpayable",
      "type": "function"
    },
    {
      "inputs": [
        {
          "internalType": "uint256",
          "name": "_newAPY",
          "type": "uint256"
        }
      ],
      "name": "setMaxAPY",
      "outputs": [],
      "stateMutability": "nonpayable",
      "type": "function"
    },
    {
      "inputs": [
        {
          "internalType": "uint256",
          "name": "_newAPY",
          "type": "uint256"
        }
      ],
      "name": "setMinAPY",
      "outputs": [],
      "stateMutability": "nonpayable",
      "type": "function"
    },
    {
      "inputs": [
        {
          "internalType": "uint256",
          "name": "_newTarget",
          "type": "uint256"
        }
      ],
      "name": "setTargetStaked",
      "outputs": [],
      "stateMutability": "nonpayable",
      "type": "function"
    },
    {
      "inputs": [
        {
          "internalType": "uint256",
          "name": "_amount",
          "type": "uint256"
        }
      ],
      "name": "stake",
      "outputs": [],
      "stateMutability": "nonpayable",
      "type": "function"
    },
    {
      "inputs": [
        {
          "internalType": "uint256",
          "name": "_amount",
          "type": "uint256"
        }
      ],
      "name": "unstake",
      "outputs": [],
      "stateMutability": "payable",
      "type": "function"
    },
    {
      "inputs": [],
      "name": "targetStaked",
      "outputs": [
        {
          "internalType": "uint256",
          "name": "",
          "type": "uint256"
        }
      ],
      "stateMutability": "view",
      "type": "function"
    },
    {
      "inputs": [],
      "name": "totalStaked",
      "outputs": [
        {
          "internalType": "uint256",
          "name": "",
          "type": "uint256"
        }
      ],
      "stateMutability": "view",
      "type": "function"
    }
  ]; // Replace with your contract's ABI

let contract;
let signer;

async function connectWallet() {
    if (typeof window.ethereum !== 'undefined') {
        try {
            await window.ethereum.request({ method: "eth_requestAccounts" });
            const provider = new ethers.providers.Web3Provider(window.ethereum);
            signer = provider.getSigner();
            contract = new ethers.Contract(contractAddress, contractABI, signer);
            console.log("Wallet connected");
            // Optionally update the UI to reflect the connected state
        } catch (error) {
            console.error("Wallet connection error:", error);
        }
    } else {
        console.log("Please install MetaMask or a compatible wallet!");
    }
}

async function stake() {
    await connectWallet(); //Ensure you've connected your wallet

    const amount = document.getElementById("stakeAmount").value;
    try {
        const tx = await contract.stake(amount);
        await tx.wait(); // Wait for the transaction to be mined
        console.log("Staked successfully!");
    } catch (error) {
        console.error("Stake error:", error);
    }
}

async function unstake() {
    await connectWallet();
    const amount = document.getElementById("unstakeAmount").value;
    try {
        const tx = await contract.unstake(amount);
        await tx.wait();
        console.log("Unstaked successfully!");
    } catch (error) {
        console.error("Unstake error:", error);
    }
}

async function getAPY() {
    await connectWallet();
    try {
        const apy = await contract.calculateAPY();
        document.getElementById("apyDisplay").innerText = "APY: " + apy + "%";
    } catch (error) {
        console.error("Get APY error:", error);
    }
}

async function getStake() {
    await connectWallet();
    try {
        const stake = await contract.getStake(await signer.getAddress());
        document.getElementById("stakeDisplay").innerText = "My Stake: " + stake;
    } catch (error) {
        console.error("Get Stake error:", error);
    }
}

async function setTarget() {
    await connectWallet();
    const newTarget = document.getElementById("targetStakedAmount").value;
    try {
        const tx = await contract.setTargetStaked(newTarget);
        await tx.wait();
        console.log("Target Staked Updated!");
    } catch (error) {
        console.error("Set Target Staked error:", error);
    }
}

async function setBase() {
    await connectWallet();
    const newBase = document.getElementById("baseAPYAmount").value;
    try {
        const tx = await contract.setBaseAPY(newBase);
        await tx.wait();
        console.log("Base APY Updated!");
    } catch (error) {
        console.error("Set Base APY error:", error);
    }
}

async function setMax() {
    await connectWallet();
    const newMax = document.getElementById("maxAPYAmount").value;
    try {
        const tx = await contract.setMaxAPY(newMax);
        await tx.wait();
        console.log("Max APY Updated!");
    } catch (error) {
        console.error("Set Max APY error:", error);
    }
}

async function setMin() {
    await connectWallet();
    const newMin = document.getElementById("minAPYAmount").value;
    try {
        const tx = await contract.setMinAPY(newMin);
        await tx.wait();
        console.log("Min APY Updated!");
    } catch (error) {
        console.error("Set Min APY error:", error);
    }
}
```

**Explanation of JavaScript Code:**

1.  **`contractAddress` and `contractABI`**:  Replace these with the actual address of your deployed smart contract and the contract's ABI (Application Binary Interface).  You get the ABI from the Solidity compiler output.
2.  **`ethers` Library**:  The code uses the `ethers` library to interact with the Ethereum blockchain.
3.  **`connectWallet()`**:  Connects to the user's Ethereum wallet (e.g., MetaMask) using `window.ethereum`.  It gets the provider and signer.
4.  **`stake()`, `unstake()`, `getAPY()`, `getStake()`, `setTarget()`, `setBase()`, `setMax()`, `setMin()`**: These functions are called when the corresponding buttons are clicked. They:
    *   Get the input values from the HTML form.
    *   Call the appropriate function on the smart contract using the `contract` instance.
    *   Wait for the transaction to be mined.
    *   Display the results in the HTML.
5.  **Error Handling**:  Includes basic `try...catch` blocks for error handling.

**How to Run:**

1.  **Deploy the Solidity Contract:**
    *   Use Remix IDE (remix.ethereum.org) or Hardhat/Truffle to compile and deploy the `DynamicAPY.sol` contract to a test network (e.g., Ganache, Rinkeby, Goerli).
    *   Note the deployed contract address.
    *   Also copy the ABI of the contract (Remix provides a "Copy to Clipboard" button for the ABI).
2.  **Set Up Your Environment:**
    *   Create an `index.html` and `app.js` file in the same directory.
    *   Paste the HTML and JavaScript code above into those files.
    *   Replace `YOUR_CONTRACT_ADDRESS` in `app.js` with the actual contract address you obtained during deployment.
    *   Replace `contractABI` with the ABI of your contract.
3.  **Serve the HTML:**
    *   You can use a simple web server to serve the `index.html` file.  For example, if you have Python installed, you can run `python -m http.server` in the directory.
4.  **Open in Browser:** Open `http://localhost:8000` (or the address shown by your web server) in your browser.
5.  **Interact:**
    *   Make sure you have MetaMask or a compatible Ethereum wallet installed and connected to the same test network where you deployed the contract.
    *   Connect your wallet.
    *   Enter amounts to stake or unstake, and click the buttons.
    *   Click "Get Current APY" to see the calculated APY.

**Important Considerations and Improvements:**

*   **Security:**  This is a simplified example and is *not* production-ready.  It lacks proper security checks and could be vulnerable to attacks.
*   **Token Handling:** This example doesn't deal with actual ERC-20 tokens.  In a real-world scenario, you'd need to integrate with an ERC-20 token contract (users would need to approve the staking contract to spend their tokens).
*   **Access Control:**  The `setTargetStaked`, `setBaseAPY`, `setMaxAPY`, and `setMinAPY` functions should be restricted to an owner or admin role using `onlyOwner` modifiers.
*   **Reentrancy:**  Be aware of reentrancy vulnerabilities, especially when transferring tokens. Use the "Checks-Effects-Interactions" pattern.
*   **Frontend Polish:** The frontend is very basic.  Use a framework like React, Vue.js, or Angular to create a more user-friendly interface.
*   **Gas Optimization:** Solidity code can be optimized to reduce gas costs.
*   **Testing:**  Write thorough unit tests for your smart contract.
*   **Error Handling:** The JavaScript error handling is rudimentary.  Provide more informative error messages to the user.
*   **Display Precision**: Be careful about displaying APY. Solidity doesn't natively support floating-point numbers. Consider using a fixed-point representation (e.g., storing the APY multiplied by 100 or 1000) and then dividing by that factor in the frontend for display.
*   **Time-Based Rewards**:  Instead of just staking/unstaking, a real-world staking contract would also calculate and distribute rewards over time based on the APY.  This would require tracking the time a user has staked and calculating their reward accrual.
*   **Periodic APY Updates:** The APY is currently recalculated every time someone stakes or unstakes. You might want to make this happen periodically (e.g., every day) to reduce gas costs. A time-based update would also prevent rapid APY fluctuations due to small stake changes.

This example provides a foundational understanding of dynamic APY adjustment in a staking contract. Remember to thoroughly research, test, and audit your code before deploying it to a production environment.
👁️ Viewed: 9

Comments