Secure Multi-Chain Staking Bridge Solidity, JavaScript
👤 Sharing: AI
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
// --- Interfaces ---
// Interface for the ERC20 token
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);
}
// Interface for the Bridge contract on other chains (simplified)
interface IBridge {
function withdraw(address recipient, uint256 amount) external;
}
// --- Contract ---
contract StakingBridge {
// --- State Variables ---
IERC20 public stakingToken; // Address of the ERC20 staking token
uint256 public minimumStakeAmount; // Minimum amount required to stake
uint256 public totalStaked; // Total amount staked in this contract
mapping(address => uint256) public stakedBalances; // Mapping of user address to their staked balance
address public owner; // Owner of the contract (to set minimum stake and withdraw fees)
IBridge public bridgeContract; // Address of the bridge contract on the destination chain
// --- Events ---
event Staked(address indexed user, uint256 amount);
event Unstaked(address indexed user, uint256 amount);
event BridgeWithdrawalInitiated(address indexed user, uint256 amount);
// --- Constructor ---
constructor(address _stakingTokenAddress, uint256 _minimumStakeAmount, address _bridgeContractAddress) {
stakingToken = IERC20(_stakingTokenAddress);
minimumStakeAmount = _minimumStakeAmount;
owner = msg.sender;
bridgeContract = IBridge(_bridgeContractAddress);
}
// --- Modifiers ---
modifier onlyOwner() {
require(msg.sender == owner, "Only owner can call this function.");
_;
}
// --- Functions ---
// Function to stake tokens
function stake(uint256 _amount) external {
require(_amount >= minimumStakeAmount, "Stake amount must be greater than or equal to the minimum stake amount.");
require(stakingToken.allowance(msg.sender, address(this)) >= _amount, "Allowance too low. Approve the StakingBridge contract to spend your tokens.");
// Transfer tokens from user to this contract
require(stakingToken.transferFrom(msg.sender, address(this), _amount), "Token transfer failed.");
// Update staked balance
stakedBalances[msg.sender] += _amount;
totalStaked += _amount;
emit Staked(msg.sender, _amount);
}
// Function to unstake tokens and bridge to another chain
function unstakeAndBridge(uint256 _amount, address _recipientOnOtherChain) external {
require(stakedBalances[msg.sender] >= _amount, "Insufficient staked balance.");
// Update staked balance
stakedBalances[msg.sender] -= _amount;
totalStaked -= _amount;
// Transfer tokens to the bridge contract. Assumes the bridge contract has the mechanism to send to the final recipient.
bridgeContract.withdraw(_recipientOnOtherChain, _amount);
emit Unstaked(msg.sender, _amount);
emit BridgeWithdrawalInitiated(msg.sender, _amount);
}
// Function to set the minimum stake amount (only owner)
function setMinimumStakeAmount(uint256 _newMinimumStakeAmount) external onlyOwner {
minimumStakeAmount = _newMinimumStakeAmount;
}
// Function to set the bridge contract address (only owner)
function setBridgeContractAddress(address _newBridgeContractAddress) external onlyOwner {
bridgeContract = IBridge(_newBridgeContractAddress);
}
// Function to withdraw any ERC20 tokens sent to this contract (only owner)
function withdrawERC20(address _tokenAddress, address _recipient, uint256 _amount) external onlyOwner {
IERC20 token = IERC20(_tokenAddress);
require(token.transfer(_recipient, _amount), "Withdrawal failed.");
}
// Fallback function to prevent accidental sending of ETH
receive() external payable {
require(msg.data.length == 0, "Do not send ETH directly to this contract.");
}
}
```
**Explanation:**
1. **`SPDX-License-Identifier: MIT`**: Specifies the license for the code. It's good practice to include this.
2. **`pragma solidity ^0.8.0;`**: Specifies the Solidity compiler version to use.
3. **`IERC20 Interface`**: Defines the standard functions for an ERC20 token. This allows the contract to interact with any ERC20 token. Important functions include:
* `totalSupply()`: Returns the total supply of tokens.
* `balanceOf(address account)`: Returns the balance of a specific address.
* `transfer(address recipient, uint256 amount)`: Transfers tokens from the contract's balance to another address.
* `allowance(address owner, address spender)`: Returns the amount of tokens that a spender is allowed to spend on behalf of an owner.
* `approve(address spender, uint256 amount)`: Allows a spender to spend a certain amount of tokens on behalf of the caller.
* `transferFrom(address sender, address recipient, uint256 amount)`: Transfers tokens from one address to another, but *only if* the contract has been approved to spend tokens on behalf of the sender.
4. **`IBridge Interface`**: A simplified interface for interacting with a bridge contract on another chain. This bridge contract is responsible for handling the transfer of assets to the destination chain and crediting the receiver. The `withdraw` function takes the receiver's address on the *other* chain and the amount to be transferred.
5. **`StakingBridge Contract`**: The main contract that handles staking and bridging.
* **`State Variables`**:
* `stakingToken`: The address of the ERC20 token that can be staked. This is initialized in the constructor.
* `minimumStakeAmount`: The minimum amount of tokens that a user must stake.
* `totalStaked`: The total amount of tokens currently staked in the contract.
* `stakedBalances`: A mapping that stores the amount of tokens each user has staked. `stakedBalances[msg.sender]` gives the staked balance of the caller.
* `owner`: The address of the contract owner, who can set the minimum stake amount and withdraw tokens sent to the contract (potentially for collecting fees, but no fees are explicitly implemented here).
* `bridgeContract`: The address of the IBridge contract on a different chain, which handles the actual bridging mechanism.
* **`Events`**: Events are emitted when important actions occur, such as staking, unstaking, and initiating a bridge withdrawal. These events are useful for tracking contract activity and for triggering off-chain actions (e.g., updating a UI or triggering a bridge relayer).
* `Staked`: Emitted when a user stakes tokens.
* `Unstaked`: Emitted when a user unstakes tokens.
* `BridgeWithdrawalInitiated`: Emitted when the unstaking and bridge withdrawal process has started.
* **`Constructor`**: The constructor is called when the contract is deployed. It initializes the `stakingToken`, `minimumStakeAmount`, `owner`, and `bridgeContract` state variables. You must pass these values when deploying the contract.
* **`Modifiers`**:
* `onlyOwner`: A modifier that restricts access to a function to only the contract owner. It checks if `msg.sender` (the address calling the function) is equal to `owner`.
* **`Functions`**:
* **`stake(uint256 _amount)`**: Allows users to stake their tokens.
* It first checks if the stake amount is greater than or equal to the minimum stake amount.
* Crucially, it checks if the user has *approved* the `StakingBridge` contract to spend their tokens using `stakingToken.allowance()`. The user needs to call `approve()` on the staking token *before* calling `stake()`.
* It transfers the tokens from the user's address to the contract's address using `stakingToken.transferFrom()`. This function requires the `allowance` check to have passed.
* It updates the `stakedBalances` and `totalStaked` variables.
* It emits the `Staked` event.
* **`unstakeAndBridge(uint256 _amount, address _recipientOnOtherChain)`**: Allows users to unstake their tokens and initiate a bridge transfer to another chain.
* It checks if the user has a sufficient staked balance.
* It updates the `stakedBalances` and `totalStaked` variables.
* It *calls* the `withdraw` function on the `bridgeContract`. This is the crucial part that interfaces with the other chain. The bridge contract is expected to handle the logic of transferring the tokens to the recipient on the other chain.
* It emits the `Unstaked` and `BridgeWithdrawalInitiated` events. The `_recipientOnOtherChain` is not explicitly stored on-chain but is passed to the bridge contract for cross-chain delivery.
* **`setMinimumStakeAmount(uint256 _newMinimumStakeAmount)`**: Allows the owner to change the minimum stake amount. It uses the `onlyOwner` modifier.
* **`setBridgeContractAddress(address _newBridgeContractAddress)`**: Allows the owner to update the bridge contract's address. Useful if the bridge needs to be upgraded or replaced.
* **`withdrawERC20(address _tokenAddress, address _recipient, uint256 _amount)`**: Allows the owner to withdraw any ERC20 tokens that have been accidentally sent to the contract or collected as fees (if a fee structure were implemented).
* **`receive() external payable`**: This is a fallback function that prevents users from accidentally sending ETH to the contract. If someone tries to send ETH, the transaction will revert with an error message. This is important for security, as the contract is not designed to handle ETH.
**Example JavaScript (using ethers.js) for interacting with the contract:**
```javascript
const { ethers } = require("ethers");
// Replace with your contract address and ABI
const STAKING_BRIDGE_ADDRESS = "0x...";
const STAKING_BRIDGE_ABI = [
// Paste your ABI here (from the Solidity compiler output)
// Example (but replace with *your* actual ABI):
{
"inputs": [
{
"internalType": "address",
"name": "_stakingTokenAddress",
"type": "address"
},
{
"internalType": "uint256",
"name": "_minimumStakeAmount",
"type": "uint256"
},
{
"internalType": "address",
"name": "_bridgeContractAddress",
"type": "address"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "user",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "BridgeWithdrawalInitiated",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "owner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "spender",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "value",
"type": "uint256"
}
],
"name": "Approval",
"type": "event"
},
{
"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": "from",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "to",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "value",
"type": "uint256"
}
],
"name": "Transfer",
"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"
},
{
"stateMutability": "payable",
"type": "fallback"
},
{
"inputs": [
{
"internalType": "address",
"name": "_tokenAddress",
"type": "address"
},
{
"internalType": "address",
"name": "_recipient",
"type": "address"
},
{
"internalType": "uint256",
"name": "_amount",
"type": "uint256"
}
],
"name": "withdrawERC20",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"stateMutability": "payable",
"type": "receive"
},
{
"inputs": [],
"name": "minimumStakeAmount",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "owner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "stakingToken",
"outputs": [
{
"internalType": "contract IERC20",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "stakedBalances",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "_amount",
"type": "uint256"
}
],
"name": "stake",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "totalStaked",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "_amount",
"type": "uint256"
},
{
"internalType": "address",
"name": "_recipientOnOtherChain",
"type": "address"
}
],
"name": "unstakeAndBridge",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
];
// Replace with your ERC20 token address and ABI
const ERC20_TOKEN_ADDRESS = "0x...";
const ERC20_TOKEN_ABI = [
// Paste your ABI here (from the Solidity compiler output)
//Example(but replace with *your* actual ABI):
{
"constant": false,
"inputs": [
{
"name": "_spender",
"type": "address"
},
{
"name": "_value",
"type": "uint256"
}
],
"name": "approve",
"outputs": [
{
"name": "success",
"type": "bool"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "totalSupply",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
}
];
async function main() {
// Connect to your Ethereum provider (e.g., MetaMask)
const provider = new ethers.providers.Web3Provider(window.ethereum);
await window.ethereum.request({ method: "eth_requestAccounts" }); // Request account access
const signer = provider.getSigner();
// Create contract instances
const stakingBridgeContract = new ethers.Contract(STAKING_BRIDGE_ADDRESS, STAKING_BRIDGE_ABI, signer);
const erc20TokenContract = new ethers.Contract(ERC20_TOKEN_ADDRESS, ERC20_TOKEN_ABI, signer);
// Example: Stake tokens
const stakeAmount = ethers.utils.parseUnits("10", 18); // Stake 10 tokens (adjust decimals)
// **IMPORTANT: Approve the StakingBridge contract to spend your ERC20 tokens first!**
const approveTx = await erc20TokenContract.approve(STAKING_BRIDGE_ADDRESS, stakeAmount);
await approveTx.wait(); // Wait for the approval transaction to be mined
const stakeTx = await stakingBridgeContract.stake(stakeAmount);
await stakeTx.wait();
console.log("Staked!");
// Example: Unstake tokens and bridge
const unstakeAmount = ethers.utils.parseUnits("5", 18); // Unstake 5 tokens (adjust decimals)
const recipientOnOtherChain = "0x..."; // Replace with the recipient's address on the other chain
const unstakeTx = await stakingBridgeContract.unstakeAndBridge(unstakeAmount, recipientOnOtherChain);
await unstakeTx.wait();
console.log("Unstaked and bridged!");
}
main().catch((error) => {
console.error(error);
});
```
**JavaScript Explanation:**
1. **Dependencies:** Requires the `ethers` library for interacting with Ethereum. Install with `npm install ethers`.
2. **Constants:** Replace placeholders with your actual contract addresses and ABIs. **The ABI is crucial.** It's the interface definition generated by the Solidity compiler that tells ethers.js how to interact with your contract.
3. **Provider and Signer:** Connects to an Ethereum provider (like MetaMask). The `signer` represents the user's account that will be signing the transactions.
4. **Contract Instances:** Creates instances of the `StakingBridge` and `IERC20` contracts using the addresses, ABIs, and signer.
5. **`stake()` example:**
* Defines the amount to stake (using `ethers.utils.parseUnits` to handle token decimals correctly).
* **Crucially, it approves the StakingBridge to spend the user's tokens *before* calling `stake()`.** This is a standard ERC20 requirement.
* Calls the `stake()` function on the `StakingBridge` contract.
* Waits for the transaction to be mined.
6. **`unstakeAndBridge()` example:**
* Defines the amount to unstake.
* Defines the recipient's address *on the other chain*. This is the address to which the tokens will be transferred on the destination chain.
* Calls the `unstakeAndBridge()` function on the `StakingBridge` contract.
* Waits for the transaction to be mined.
7. **Error Handling:** Includes a `try...catch` block to handle potential errors.
**Important Considerations:**
* **Security:** This is a simplified example. A production-ready staking bridge would require extensive security audits, input validation, and protection against vulnerabilities like reentrancy attacks.
* **Bridge Design:** The bridge contract on the other chain (represented by the `IBridge` interface) is a crucial part of the system. Its implementation will depend on the specific bridging technology being used. Considerations include:
* **Token wrapping/unwrapping:** How are tokens represented on the other chain? Are they wrapped versions of the original tokens?
* **Message passing:** How is the information about the withdrawal transferred to the other chain? (e.g., using relayers, cross-chain communication protocols)
* **Consensus mechanisms:** How is the withdrawal verified on the other chain?
* **Gas Costs:** Cross-chain transactions can be expensive due to gas costs on both chains. Consider optimizing the contract code and bridge design to minimize gas usage.
* **Decimals:** Be very careful with token decimals. `ethers.utils.parseUnits` and `ethers.utils.formatUnits` are helpful for converting between human-readable numbers and the integer representation used by smart contracts. Always specify the correct number of decimals for the token.
* **Front-End Integration:** You'll need a front-end (using React, Vue.js, or similar) to allow users to interact with the contract. The front-end would handle connecting to a wallet, displaying token balances, and calling the contract functions.
* **Events:** Listen for the events emitted by the contract (e.g., `Staked`, `Unstaked`, `BridgeWithdrawalInitiated`) to update your UI and track the progress of transactions.
This improved example provides a much more complete and understandable starting point for building a secure and functional multi-chain staking bridge. Remember to thoroughly test and audit your code before deploying it to a live environment.
👁️ Viewed: 8
Comments