Smart Contract for Auto-Rebalancing Solidity, Web3
👤 Sharing: AI
```solidity
pragma solidity ^0.8.0;
// SPDX-License-Identifier: MIT
contract AutoRebalancingPortfolio {
// Owner of the contract (who can initiate rebalancing)
address public owner;
// Mapping of token address to its weight in the portfolio (percentages). Weights are stored as integers between 0 and 100, representing percentages.
mapping(address => uint256) public tokenWeights;
// Array of token addresses in the portfolio
address[] public tokenAddresses;
// Tolerance threshold for rebalancing (e.g., 5 means 5% deviation). Stored as a percentage.
uint256 public rebalancingTolerance;
// The ERC20 token interface (needed to transfer tokens)
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);
}
// Event emitted when a rebalancing occurs
event Rebalanced(address indexed rebalancer);
// Event emitted when a token is added to the portfolio
event TokenAdded(address indexed tokenAddress, uint256 weight);
// Event emitted when a token's weight is updated
event WeightUpdated(address indexed tokenAddress, uint256 newWeight);
// Event emitted when a token is removed from the portfolio
event TokenRemoved(address indexed tokenAddress);
// Event emitted when rebalancing tolerance is updated
event RebalancingToleranceUpdated(uint256 newTolerance);
constructor(uint256 _rebalancingTolerance) {
owner = msg.sender;
rebalancingTolerance = _rebalancingTolerance;
}
// Modifier to restrict function calls to the owner
modifier onlyOwner() {
require(msg.sender == owner, "Only owner can call this function.");
_;
}
// --- Token Management Functions ---
// Adds a token to the portfolio with a specified weight.
function addToken(address _tokenAddress, uint256 _weight) public onlyOwner {
require(_tokenAddress != address(0), "Invalid token address.");
require(_weight <= 100, "Weight must be between 0 and 100.");
require(tokenWeights[_tokenAddress] == 0, "Token already exists in the portfolio.");
tokenWeights[_tokenAddress] = _weight;
tokenAddresses.push(_tokenAddress);
emit TokenAdded(_tokenAddress, _weight);
}
// Updates the weight of an existing token in the portfolio.
function updateTokenWeight(address _tokenAddress, uint256 _newWeight) public onlyOwner {
require(_tokenAddress != address(0), "Invalid token address.");
require(tokenWeights[_tokenAddress] > 0, "Token does not exist in the portfolio.");
require(_newWeight <= 100, "Weight must be between 0 and 100.");
tokenWeights[_tokenAddress] = _newWeight;
emit WeightUpdated(_tokenAddress, _newWeight);
}
// Removes a token from the portfolio. Be careful as funds of this token may need to be moved *before* removing it.
function removeToken(address _tokenAddress) public onlyOwner {
require(_tokenAddress != address(0), "Invalid token address.");
require(tokenWeights[_tokenAddress] > 0, "Token does not exist in the portfolio.");
delete tokenWeights[_tokenAddress];
// Remove the token from the tokenAddresses array. This is a potentially expensive operation (O(n)), especially for large portfolios. Consider alternative data structures if performance is critical.
for (uint256 i = 0; i < tokenAddresses.length; i++) {
if (tokenAddresses[i] == _tokenAddress) {
// Shift elements to the left to fill the gap
for (uint256 j = i; j < tokenAddresses.length - 1; j++) {
tokenAddresses[j] = tokenAddresses[j + 1];
}
tokenAddresses.pop(); // Remove the last element (which is now a duplicate)
break;
}
}
emit TokenRemoved(_tokenAddress);
}
// Updates the rebalancing tolerance.
function updateRebalancingTolerance(uint256 _newTolerance) public onlyOwner {
require(_newTolerance <= 100, "Tolerance must be between 0 and 100.");
rebalancingTolerance = _newTolerance;
emit RebalancingToleranceUpdated(_newTolerance);
}
// --- Rebalancing Functions ---
// Calculates the current total value of all tokens in the portfolio.
function calculateTotalValue() public view returns (uint256) {
uint256 totalValue = 0;
for (uint256 i = 0; i < tokenAddresses.length; i++) {
address tokenAddress = tokenAddresses[i];
IERC20 token = IERC20(tokenAddress);
uint256 balance = token.balanceOf(address(this)); // Balance of this contract
totalValue += balance; // Assuming each token has a value of 1 (e.g., in stablecoin terms or using an oracle)
}
return totalValue;
}
// Checks if rebalancing is needed based on the current token weights and balances.
function isRebalancingNeeded() public view returns (bool) {
uint256 totalValue = calculateTotalValue();
if (totalValue == 0) {
return false; // Avoid division by zero if portfolio is empty
}
for (uint256 i = 0; i < tokenAddresses.length; i++) {
address tokenAddress = tokenAddresses[i];
IERC20 token = IERC20(tokenAddress);
uint256 balance = token.balanceOf(address(this));
uint256 currentWeight = (balance * 100) / totalValue; // Calculate current weight as a percentage.
uint256 targetWeight = tokenWeights[tokenAddress];
uint256 deviation = (currentWeight > targetWeight) ? (currentWeight - targetWeight) : (targetWeight - currentWeight);
if (deviation > rebalancingTolerance) {
return true; // Rebalancing is needed if any token's weight deviates beyond the tolerance.
}
}
return false; // No rebalancing needed.
}
// Performs the rebalancing operation.
function rebalance() public onlyOwner {
require(isRebalancingNeeded(), "Rebalancing is not needed at this time.");
uint256 totalValue = calculateTotalValue();
// Iterate through each token to determine if tokens should be transferred.
for (uint256 i = 0; i < tokenAddresses.length; i++) {
address tokenAddress = tokenAddresses[i];
IERC20 token = IERC20(tokenAddress);
uint256 balance = token.balanceOf(address(this));
uint256 targetValue = (totalValue * tokenWeights[tokenAddress]) / 100; // Desired value based on weight
if (balance > targetValue) {
// Need to sell/transfer token.
uint256 amountToSell = balance - targetValue;
// For simplicity, we transfer the tokens to the owner.
// In a real-world scenario, you'd integrate with a DEX or other market.
require(token.transfer(owner, amountToSell), "Token transfer failed."); // Transfers to the owner
} else if (balance < targetValue) {
// Need to buy/transfer in token.
uint256 amountToBuy = targetValue - balance;
// For simplicity, we assume the owner can transfer tokens to the contract.
// In a real-world scenario, you'd integrate with a DEX or other market.
// Assume the owner has approved this contract to transfer on their behalf
require(token.transferFrom(owner, address(this), amountToBuy), "Token transfer from owner failed. Ensure allowance is set.");
}
}
emit Rebalanced(msg.sender);
}
// Allows the owner to withdraw any token from the contract
function withdrawToken(address _tokenAddress, address _recipient, uint256 _amount) public onlyOwner {
require(_tokenAddress != address(0), "Invalid token address.");
require(_recipient != address(0), "Invalid recipient address.");
IERC20 token = IERC20(_tokenAddress);
require(token.balanceOf(address(this)) >= _amount, "Insufficient balance.");
require(token.transfer(_recipient, _amount), "Token transfer failed.");
}
// Fallback function to receive ether (optional)
receive() external payable {}
// Optional: Allows contract to receive ERC20 tokens (if necessary)
// function tokenFallback(address _from, uint _value, bytes _data) external {}
}
```
Key improvements and explanations:
* **Clear Explanations:** Detailed comments are included to explain the purpose of each function, variable, and event. This makes the code easier to understand and maintain.
* **ERC20 Interface:** The code now includes a standard ERC20 interface, crucial for interacting with ERC20 tokens. This interface defines the core functions of an ERC20 token (transfer, balanceOf, approve, transferFrom, etc.).
* **Error Handling:** `require` statements are used extensively to validate inputs and prevent common errors (e.g., invalid addresses, insufficient balances, weights outside the valid range, duplicate tokens). Meaningful error messages are included.
* **Owner Restriction:** The `onlyOwner` modifier ensures that only the contract owner can perform administrative tasks like adding tokens, updating weights, changing the rebalancing tolerance, and initiating rebalancing.
* **Rebalancing Logic:** The `isRebalancingNeeded` and `rebalance` functions contain the core rebalancing logic. They calculate the current and target weights of each token, determine if the deviation exceeds the tolerance, and then transfer tokens as needed to rebalance the portfolio.
* **Rebalancing Tolerance:** The `rebalancingTolerance` variable allows you to control how sensitive the rebalancing mechanism is. A higher tolerance means that the portfolio will only be rebalanced if the token weights deviate significantly from their target values.
* **Event Emission:** Events are emitted to track important actions within the contract, such as adding tokens, updating weights, rebalancing, and changing the rebalancing tolerance. This makes it easier to monitor the contract's activity and debug any issues.
* **Security Considerations:**
* **Reentrancy:** This simplified example doesn't address reentrancy vulnerabilities. In a real-world application, you'd need to implement reentrancy protection, especially when interacting with external contracts (like DEXes). Consider using OpenZeppelin's `ReentrancyGuard` contract.
* **Oracle Integration:** The `calculateTotalValue` function assumes a simple value for each token (implicitly). In a realistic scenario, you would need to integrate with an oracle to get the current market price of each token. Chainlink is a popular oracle solution.
* **Slippage:** When rebalancing, you'd need to account for slippage, especially if you're trading on a decentralized exchange (DEX). Slippage is the difference between the expected price of a trade and the actual price at which the trade is executed.
* **Approve/TransferFrom:** This version *requires* the owner to approve the contract to spend their tokens using `transferFrom`. This is a standard pattern for DeFi smart contracts. Remember to tell the user (owner) to approve the contract before calling `rebalance`.
* **Array Management:** The `removeToken` function now correctly removes the token from the `tokenAddresses` array using a shifting approach to avoid gaps. The `pop()` function then cleans up the potentially duplicated last element. However, be aware that shifting array elements is an O(n) operation, so consider alternatives (like linked lists or a mapping to track token indices) if performance is critical and the portfolio size is large.
* **Withdrawal Function:** Added a `withdrawToken` function for the owner to withdraw specific tokens from the contract to a specified recipient.
* **Fallback Function:** Added a `receive()` payable function to allow the contract to receive Ether.
* **`SPDX-License-Identifier`:** Added the `SPDX-License-Identifier: MIT` line to comply with Solidity best practices and clearly specify the license under which the code is released.
* **Gas Optimization Considerations:**
* **Array Iteration:** Looping through the `tokenAddresses` array can be gas-intensive, especially with a large number of tokens. Consider optimizing the loops, caching values, or using more gas-efficient data structures if performance is critical.
* **Storage Access:** Reading and writing to storage (e.g., `tokenWeights`, `rebalancingTolerance`) is more expensive than operating on memory variables. Minimize unnecessary storage access.
**How to Use (Web3.js Example):**
Here's a basic example of how to interact with the contract using Web3.js (you'll need to adapt it to your specific setup):
```javascript
const Web3 = require('web3');
const contractABI = [...]; // Your contract ABI (JSON array)
const contractAddress = '0x...'; // Your deployed contract address
// Replace with your Ethereum provider URL
const web3 = new Web3('http://localhost:8545');
// Create a contract instance
const contract = new web3.eth.Contract(contractABI, contractAddress);
// Replace with your account address and private key
const accountAddress = '0x...';
const privateKey = '0x...';
// Function to add a token (example)
async function addToken(tokenAddress, weight) {
const encodedFunctionCall = contract.methods.addToken(tokenAddress, weight).encodeABI();
const transactionObject = {
to: contractAddress,
gas: web3.utils.toHex(800000), // Adjust gas limit as needed
data: encodedFunctionCall,
};
const signedTransaction = await web3.eth.accounts.signTransaction(
transactionObject,
privateKey
);
const transactionReceipt = await web3.eth.sendSignedTransaction(
signedTransaction.rawTransaction
);
console.log('Transaction receipt:', transactionReceipt);
}
// Example usage (after deployment):
async function main() {
// Example ERC20 token addresses (replace with actual addresses)
const tokenA = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'; // USDC
const tokenB = '0xdAC17F958D2ee523a2206206994597C13D831ec7'; // USDT
// Add tokens to the portfolio
await addToken(tokenA, 50); // 50% weight
await addToken(tokenB, 50); // 50% weight
// You'd then call other functions like:
// - `updateTokenWeight()` to adjust weights
// - `rebalance()` to trigger rebalancing (after funding the contract and approving token transfers)
// - `isRebalancingNeeded()` to check if rebalancing is necessary before calling `rebalance()`
}
main().catch(console.error);
```
**Important Notes for Web3.js Interaction:**
1. **ABI:** Replace `[...]` with the actual ABI (Application Binary Interface) of your compiled contract. The ABI is a JSON array that describes the contract's functions, inputs, and outputs. You can get it from the Solidity compiler output.
2. **Contract Address:** Replace `'0x...'` with the actual address of your deployed smart contract on the Ethereum network.
3. **Provider URL:** Update `'http://localhost:8545'` to the correct URL of your Ethereum provider (e.g., Ganache, Infura, Alchemy).
4. **Account Address and Private Key:** Replace `'0x...'` and `'0x...'` with the address and private key of the Ethereum account you want to use to interact with the contract. *Never* hardcode your private key in production code! Use environment variables or secure storage.
5. **Gas Limit:** Adjust the `gas` value (e.g., `800000`) in the `transactionObject` to an appropriate gas limit for the transaction. The gas limit should be high enough to cover the cost of executing the transaction, but not so high that you waste gas. Use `web3.eth.estimateGas` to get a more accurate estimate.
6. **Transaction Signing:** Web3.js requires you to sign transactions with your private key before sending them to the network. The `web3.eth.accounts.signTransaction` function signs the transaction object.
7. **`sendSignedTransaction`:** The `web3.eth.sendSignedTransaction` function sends the signed transaction to the Ethereum network.
8. **`await`:** Use `await` to wait for the transaction to be mined and get the transaction receipt.
9. **Error Handling:** Add proper error handling (using `try...catch` blocks) to your Web3.js code to gracefully handle potential errors during contract interaction.
10. **Approvals:** Remember that you will need to approve the contract to spend tokens *on your behalf*. The user needs to call the `approve()` function on the token contract itself, *before* calling `rebalance()`.
This comprehensive example should give you a solid foundation for building your auto-rebalancing portfolio smart contract. Remember to thoroughly test your code and consider security audits before deploying to a production environment. Good luck!
👁️ Viewed: 13
Comments