Copy // SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./interfaces/ICrossChainAggregator.sol";
import "./interfaces/IIU2UGateway.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract CrossChainYieldFarm is ReentrancyGuard {
struct Pool {
address stakingToken;
address rewardToken;
uint256 rewardRate;
uint256 totalStaked;
uint256 lastUpdateTime;
uint256 rewardPerTokenStored;
bool isActive;
string destinationChain;
}
struct UserInfo {
uint256 stakedAmount;
uint256 rewardPerTokenPaid;
uint256 rewards;
uint256 lastStakeTime;
}
ICrossChainAggregator public immutable aggregator;
IIU2UGateway public immutable gateway;
mapping(uint256 => Pool) public pools;
mapping(uint256 => mapping(address => UserInfo)) public userInfo;
uint256 public poolCount;
event Staked(address indexed user, uint256 indexed poolId, uint256 amount);
event Withdrawn(address indexed user, uint256 indexed poolId, uint256 amount);
event RewardsClaimed(address indexed user, uint256 indexed poolId, uint256 amount);
event CrossChainStakingInitiated(address indexed user, uint256 indexed poolId, string destinationChain);
constructor(address _aggregator, address _gateway) {
aggregator = ICrossChainAggregator(_aggregator);
gateway = IIU2UGateway(_gateway);
}
function createPool(
address stakingToken,
address rewardToken,
uint256 rewardRate,
string calldata destinationChain
) external returns (uint256 poolId) {
poolId = poolCount++;
pools[poolId] = Pool({
stakingToken: stakingToken,
rewardToken: rewardToken,
rewardRate: rewardRate,
totalStaked: 0,
lastUpdateTime: block.timestamp,
rewardPerTokenStored: 0,
isActive: true,
destinationChain: destinationChain
});
}
function stake(uint256 poolId, uint256 amount) external nonReentrant {
require(pools[poolId].isActive, "Pool not active");
require(amount > 0, "Cannot stake 0");
_updateReward(poolId, msg.sender);
Pool storage pool = pools[poolId];
UserInfo storage user = userInfo[poolId][msg.sender];
IERC20(pool.stakingToken).transferFrom(msg.sender, address(this), amount);
user.stakedAmount += amount;
pool.totalStaked += amount;
user.lastStakeTime = block.timestamp;
emit Staked(msg.sender, poolId, amount);
}
function crossChainStake(
uint256 poolId,
uint256 amount,
address tokenToSwap,
uint8 routerType
) external payable nonReentrant {
require(pools[poolId].isActive, "Pool not active");
require(amount > 0, "Cannot stake 0");
Pool storage pool = pools[poolId];
// Step 1: Swap tokens to staking token if needed
if (tokenToSwap != pool.stakingToken) {
IERC20(tokenToSwap).transferFrom(msg.sender, address(this), amount);
IERC20(tokenToSwap).approve(address(aggregator), amount);
ICrossChainAggregator.SwapParams memory swapParams = ICrossChainAggregator.SwapParams({
tokenIn: tokenToSwap,
tokenOut: pool.stakingToken,
amountIn: amount,
minAmountOut: 0, // Should calculate based on slippage
routerType: routerType,
to: address(this),
deadline: block.timestamp + 300,
swapData: ""
});
amount = aggregator.executeSwap(swapParams);
} else {
IERC20(tokenToSwap).transferFrom(msg.sender, address(this), amount);
}
// Step 2: Transfer to destination chain if needed
if (bytes(pool.destinationChain).length > 0) {
IERC20(pool.stakingToken).approve(address(gateway), amount);
IIU2UGateway.TransferParams memory transferParams = IIU2UGateway.TransferParams({
destinationChain: pool.destinationChain,
destinationAddress: address(this).toString(),
symbol: getTokenSymbol(pool.stakingToken),
amount: amount,
payload: abi.encode(msg.sender, poolId, amount)
});
gateway.transferTokens{value: msg.value}(transferParams);
emit CrossChainStakingInitiated(msg.sender, poolId, pool.destinationChain);
} else {
// Local staking
_stake(poolId, msg.sender, amount);
}
}
function withdraw(uint256 poolId, uint256 amount) external nonReentrant {
UserInfo storage user = userInfo[poolId][msg.sender];
require(user.stakedAmount >= amount, "Insufficient staked amount");
_updateReward(poolId, msg.sender);
Pool storage pool = pools[poolId];
user.stakedAmount -= amount;
pool.totalStaked -= amount;
IERC20(pool.stakingToken).transfer(msg.sender, amount);
emit Withdrawn(msg.sender, poolId, amount);
}
function claimRewards(uint256 poolId) external nonReentrant {
_updateReward(poolId, msg.sender);
UserInfo storage user = userInfo[poolId][msg.sender];
uint256 reward = user.rewards;
if (reward > 0) {
user.rewards = 0;
Pool storage pool = pools[poolId];
IERC20(pool.rewardToken).transfer(msg.sender, reward);
emit RewardsClaimed(msg.sender, poolId, reward);
}
}
function _stake(uint256 poolId, address user, uint256 amount) internal {
_updateReward(poolId, user);
Pool storage pool = pools[poolId];
UserInfo storage userStake = userInfo[poolId][user];
userStake.stakedAmount += amount;
pool.totalStaked += amount;
userStake.lastStakeTime = block.timestamp;
emit Staked(user, poolId, amount);
}
function _updateReward(uint256 poolId, address user) internal {
Pool storage pool = pools[poolId];
UserInfo storage userStake = userInfo[poolId][user];
pool.rewardPerTokenStored = rewardPerToken(poolId);
pool.lastUpdateTime = block.timestamp;
if (user != address(0)) {
userStake.rewards = earned(poolId, user);
userStake.rewardPerTokenPaid = pool.rewardPerTokenStored;
}
}
function rewardPerToken(uint256 poolId) public view returns (uint256) {
Pool storage pool = pools[poolId];
if (pool.totalStaked == 0) {
return pool.rewardPerTokenStored;
}
return pool.rewardPerTokenStored +
(((block.timestamp - pool.lastUpdateTime) * pool.rewardRate * 1e18) / pool.totalStaked);
}
function earned(uint256 poolId, address user) public view returns (uint256) {
UserInfo storage userStake = userInfo[poolId][user];
return (userStake.stakedAmount *
(rewardPerToken(poolId) - userStake.rewardPerTokenPaid)) / 1e18 + userStake.rewards;
}
function getTokenSymbol(address token) internal view returns (string memory) {
(bool success, bytes memory data) = token.staticcall(abi.encodeWithSignature("symbol()"));
if (success && data.length > 0) {
return abi.decode(data, (string));
}
return "UNKNOWN";
}
}