Copy // SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
interface ISwapContract {
function transferToStake(string memory _internalId) external;
}
contract StakeV2Contract is Ownable, ReentrancyGuard {
using SafeERC20 for IERC20;
string public DESTINATION_NETWORK = "L1X";
string public NATIVE_ASSET_SYMBOL = "ETH";
string public NATIVE_SYMBOL = "ETH";
address public constant NATIVE_ASSET_ADDRESS = address(0);
uint256 public NATIVE_ASSET_DECIMAL = 18;
address public SWAP_CONTRACT_ADDRESS = address(0);
uint256 public depositFeePercent;
// Withdrawal Penalties
uint256 public withdrawalPenaltyInDays;
uint256 public withdrawalPenaltyInPercentage;
// Mapping to store asset config information
mapping(address => mapping(uint256 => AssetConfiguration)) public assetConfig;
// Mapping to store asset capping
mapping(address => CappingConfiguration) public assetDepositCapConfig;
// Mapping to store user staking information
mapping(string => Stake) userStakesByTxId;
// Store Flagged Withdrawl
mapping(string => FlagWithdrawal) stakeFlagByTxId;
// Authorized caller
mapping(address => bool) public authorizedCaller;
// Treasury contract address
address public treasuryContract;
// Pause Feature
bool public isDepositPaused;
bool public isWithdrawalPaused;
// Structure to store staking details
// Note: Native Asset is represented by address(0)
struct Stake {
uint256 startDate;
uint256 durationInDays;
uint256 amount;
address assetAddress;
address senderAddress;
address rewardAddress;
string assetSymbol;
uint256 assetDecimal;
uint256 apr;
bool isERC20;
bool isRedemed;
uint256 redemedTimestamp;
uint256 penaltyAmount;
uint256 penaltyPercentage;
uint256 depositFeePercent;
}
struct FlagWithdrawal {
bool holdWithdrawl;
string message;
address caller;
}
struct AssetConfiguration {
uint16 apr;
uint16 treasuryPercentage;
}
struct CappingConfiguration {
uint256 maxCap;
uint256 totalDeposited;
}
event StakeDeposited(
string internalId,
address indexed walletAddress,
address indexed assetAddress, // 0x0 in case of Native
address rewardAddress,
string assetSymbol,
uint256 assetDecimal,
uint256 apr,
uint256 amount,
uint256 durationInDays,
uint256 depositFeePercent,
uint256 stakeType, // 0 = Native , 1 - ERC-20
uint256 timestamp
);
event StakeWithdraw(
string internalId,
address indexed walletAddress,
address indexed assetAddress, // 0x0 in case of Native
address rewardAddress,
string assetSymbol,
uint256 assetDecimal,
uint256 apr,
uint256 amount,
uint256 amountTransferred,
uint256 penaltyAmount,
uint256 durationInDays,
uint256 withdrawalPenaltyInPercentage,
uint256 stakeType, // 0 = Native , 1 - ERC-20
uint256 timestamp
);
event AuthorizationUpdated(
address indexed sender,
bool status
);
// XTalk Events
event XTalkMessageBroadcasted(
bytes message,
string destinationNetwork,
string destinationSmartContractAddress
);
event SwapContractAddress ( address swapContract );
modifier onlyAuthorizedCaller() {
require(
msg.sender == owner() || authorizedCaller[msg.sender] == true,
"Not authorized"
);
_;
}
constructor(
string memory _nativeSymbol,
string memory _nativeAssetSymbol,
uint256 _nativeAssetDecimal,
address _treasuryContract,
uint256 _depositFeePercent,
uint256 _withdrawalPenaltyInDays,
uint256 _withdrawalPenaltyInPercentage
) Ownable(msg.sender) {
NATIVE_SYMBOL = _nativeSymbol;
NATIVE_ASSET_SYMBOL = _nativeAssetSymbol;
NATIVE_ASSET_DECIMAL = _nativeAssetDecimal;
treasuryContract = _treasuryContract;
depositFeePercent = _depositFeePercent;
// Withdrawal Penalty configuration
withdrawalPenaltyInDays = _withdrawalPenaltyInDays;
withdrawalPenaltyInPercentage = _withdrawalPenaltyInPercentage;
}
function getStakeDetails(
string memory internalId
) public view returns (Stake memory) {
return userStakesByTxId[internalId];
}
function getStakeWithdrawFlaggedDetails(
string memory internalId
) public view returns (FlagWithdrawal memory) {
return stakeFlagByTxId[internalId];
}
// Owner can configure deposit status
function pauseDeposit(
bool depositStatus
) external onlyAuthorizedCaller {
isDepositPaused = depositStatus;
}
// Owner can configure withdrawal status
function pauseWithdrawal(
bool withdrawalStatus
) external onlyAuthorizedCaller {
isWithdrawalPaused = withdrawalStatus;
}
// Owner can configure withdrawal penalty in days
function setMinimumWithdrawalDays(
uint256 penaltyInDays
) external onlyAuthorizedCaller {
withdrawalPenaltyInDays = penaltyInDays;
}
// Owner can configure withdrawal penalty in percentage
function setWithdrawalPenaltyPercent(
uint256 penaltyInPercentage
) external onlyAuthorizedCaller {
require(
penaltyInPercentage >= 0 && penaltyInPercentage <= 100,
"Percent should be between 0 and 100"
);
withdrawalPenaltyInPercentage = penaltyInPercentage;
}
// Owner can configure staking treasury contract
function setTreasuryAddress(
address treasuryContractAddress
) external onlyAuthorizedCaller {
treasuryContract = treasuryContractAddress;
}
// Owner can configure deposit fee percent
function setDepositFeePercent(
uint256 _depositFeePercent
) external onlyAuthorizedCaller {
require(
_depositFeePercent <= 100,
"Deposit Fee should be less than 100"
);
depositFeePercent = _depositFeePercent;
}
// Update the Gas Price Contract address (only callable by the owner)
function updateSwapContractAddress(
address newAddress
) external onlyAuthorizedCaller {
require(
newAddress != address(0),
"Address cannot be the zero address."
);
SWAP_CONTRACT_ADDRESS = newAddress;
emit SwapContractAddress(newAddress);
}
// Function to transfer asset to treasury
function transferToTreasury(
address tokenAddress,
uint256 amount
) external onlyAuthorizedCaller {
require(treasuryContract != address(0), "Treasury address is not set");
if (tokenAddress == NATIVE_ASSET_ADDRESS) {
// Send Native to Treasury with reentrancy Protection for Native
(bool sent, bytes memory data) = treasuryContract.call{
value: amount
}("");
} else {
// Send Native to Treasury with reentrancy Protection for ERC20
IERC20(tokenAddress).safeTransfer(treasuryContract, amount);
}
}
// Pause and Start withdraw for perticular stake ID
function flagWithdrawRequest(
string memory internalId,
bool holdWithdrawl,
string memory message
) public onlyAuthorizedCaller returns (bool) {
stakeFlagByTxId[internalId].holdWithdrawl = holdWithdrawl;
stakeFlagByTxId[internalId].message = message;
stakeFlagByTxId[internalId].caller = msg.sender;
return true;
}
// Set locking configuration
function setAssetConfiguration(
address assetAddress,
uint256 durationInDays,
uint16 apr,
uint16 treasuryPercentage
) external onlyAuthorizedCaller {
assetConfig[assetAddress][durationInDays] = AssetConfiguration(apr, treasuryPercentage);
}
function bulkAssetConfigurationUpdate(
address[] memory assetAddress,
uint256[] memory maxCap,
uint256[] memory durationInDays,
uint16[][] memory apr,
uint16[][] memory treasuryPercentage
) public onlyAuthorizedCaller returns (bool) {
require(
assetAddress.length == maxCap.length &&
apr.length == treasuryPercentage.length,
"Array lengths must match"
);
for (uint256 _addressIndex = 0; _addressIndex < assetAddress.length; _addressIndex++) {
address _assetAddress = assetAddress[_addressIndex];
assetDepositCapConfig[_assetAddress].maxCap = maxCap[_addressIndex];
for (uint256 _daysIndex = 0; _daysIndex < durationInDays.length; _daysIndex++) {
uint256 _durationInDays = durationInDays[_daysIndex];
uint16 _apr = apr[_addressIndex][_daysIndex];
uint16 _treasuryPercentage = treasuryPercentage[_addressIndex][_daysIndex];
assetConfig[_assetAddress][_durationInDays] = AssetConfiguration(_apr, _treasuryPercentage);
}
}
}
// Set asset cap
function setAssetDepositCapConfig(
address assetAddress,
uint256 maxCap
) external onlyAuthorizedCaller {
assetDepositCapConfig[assetAddress].maxCap = maxCap;
}
// Set asset cap
function resetAssetDepositedAmount(
address assetAddress,
uint256 totalDeposited
) external onlyAuthorizedCaller {
assetDepositCapConfig[assetAddress].totalDeposited = totalDeposited;
}
// Set an authorized caller
function setAuthorizedCaller(
address sender,
bool status
) external onlyOwner {
authorizedCaller[sender] = status;
emit AuthorizationUpdated(sender, status);
}
// User can deposit stake (ERC20)
function depositForERC20(
uint256 durationInDays,
address rewardAddress,
string memory assetSymbol,
address assetAddress,
uint256 amount,
uint256 decimals,
string memory internalId
) external nonReentrant {
require(isDepositPaused == false, "Deposit is paused");
require(userStakesByTxId[internalId].amount == 0, "Stake with given internal ID already exist");
require(amount > 0, "Amount must be greater than zero.");
uint256 totalDeposit = amount + assetDepositCapConfig[assetAddress].totalDeposited;
require(totalDeposit <= assetDepositCapConfig[assetAddress].maxCap, "Max capping reached. Please try with lesser amount");
require(decimals > 0, "Decimals must be greater than 0");
require(rewardAddress != address(0), "Invalid Reward Address");
require(assetAddress != address(0), "Invalid Asset Address");
require(SWAP_CONTRACT_ADDRESS != address(0), "Invalid Swap Address");
require(isEmptyString(assetSymbol) == false, "Invalid Asset Symbol");
// Call the internal transfer function
require(
transferAsset(
assetAddress,
msg.sender,
address(this),
amount,
0
),
"Transfer failed"
);
// Handle Treeasury Share
uint256 swapDistribution = calulatePercentAmount(assetConfig[assetAddress][durationInDays].treasuryPercentage, amount);
if (swapDistribution > 0) {
transferAssetForWithdrawal(
assetAddress,
address(this),
SWAP_CONTRACT_ADDRESS,
swapDistribution
);
}
userStakesByTxId[internalId] = Stake(
getCurrentTimestamp(),
durationInDays,
amount,
assetAddress,
msg.sender,
rewardAddress,
assetSymbol,
decimals,
assetConfig[assetAddress][durationInDays].apr,
true,
false,
0,
0,
0,
depositFeePercent
);
assetDepositCapConfig[assetAddress].totalDeposited = totalDeposit;
bytes memory messageBytes = abi.encode(
internalId,
msg.sender,
assetAddress,
rewardAddress,
NATIVE_SYMBOL,
assetSymbol,
decimals,
assetConfig[assetAddress][durationInDays].apr,
amount,
durationInDays,
depositFeePercent,
1,
getCurrentTimestamp()
);
// Broadcast cross-network message
emit XTalkMessageBroadcasted(messageBytes, DESTINATION_NETWORK, '');
emit StakeDeposited(
internalId,
msg.sender,
assetAddress,
rewardAddress,
assetSymbol,
decimals,
assetConfig[assetAddress][durationInDays].apr,
amount,
durationInDays,
depositFeePercent,
1,
getCurrentTimestamp()
);
}
// User can deposit stake (Native)
function depositForNative(
uint256 durationInDays,
address rewardAddress,
string memory internalId
) external payable nonReentrant {
require(isDepositPaused == false, "Deposit is paused");
require(userStakesByTxId[internalId].amount == 0, "Stake with given internal ID already exist");
require(rewardAddress != address(0), "Invalid Reward Address");
require(SWAP_CONTRACT_ADDRESS != address(0), "Invalid Swap Address");
uint256 amount = msg.value;
require(amount > 0, "Amount must be greater than zero.");
uint256 totalDeposit = amount + assetDepositCapConfig[NATIVE_ASSET_ADDRESS].totalDeposited;
require(totalDeposit <= assetDepositCapConfig[NATIVE_ASSET_ADDRESS].maxCap, "Max capping reached. Please try with lesser amount");
// Handle Treeasury Share
uint256 swapDistribution = calulatePercentAmount(assetConfig[NATIVE_ASSET_ADDRESS][durationInDays].treasuryPercentage, amount);
if (swapDistribution > 0) {
uint256 nativeAmount = msg.value;
// Call the internal transfer function
require(
transferAsset(
NATIVE_ASSET_ADDRESS,
msg.sender,
SWAP_CONTRACT_ADDRESS,
swapDistribution,
nativeAmount
),
"Transfer failed"
);
}
// Store Stake
userStakesByTxId[internalId] = Stake(
getCurrentTimestamp(),
durationInDays,
amount,
NATIVE_ASSET_ADDRESS,
msg.sender,
rewardAddress,
NATIVE_ASSET_SYMBOL,
NATIVE_ASSET_DECIMAL,
assetConfig[NATIVE_ASSET_ADDRESS][durationInDays].apr,
false,
false,
0,
0,
0,
depositFeePercent
);
assetDepositCapConfig[NATIVE_ASSET_ADDRESS].totalDeposited = totalDeposit;
bytes memory messageBytes = abi.encode(
internalId,
msg.sender,
NATIVE_ASSET_ADDRESS,
rewardAddress,
NATIVE_SYMBOL,
NATIVE_ASSET_SYMBOL,
NATIVE_ASSET_DECIMAL,
assetConfig[NATIVE_ASSET_ADDRESS][durationInDays].apr,
amount,
durationInDays,
depositFeePercent,
0,
getCurrentTimestamp()
);
// Broadcast cross-network message
emit XTalkMessageBroadcasted(messageBytes, DESTINATION_NETWORK, '');
emit StakeDeposited(
internalId,
msg.sender,
NATIVE_ASSET_ADDRESS,
rewardAddress,
NATIVE_ASSET_SYMBOL,
NATIVE_ASSET_DECIMAL,
assetConfig[NATIVE_ASSET_ADDRESS][durationInDays].apr,
amount,
durationInDays,
depositFeePercent,
0,
getCurrentTimestamp()
);
}
function checkWithdrawStakeById(string memory internalId) public view returns(bool) {
require(
isStakeUnlocked(internalId) == true,
"Given Stake is not available for withdrawal yet"
);
// Retrieve Stake
Stake memory stake = userStakesByTxId[internalId];
return stake.isRedemed;
}
function withdrawStakeById(string memory internalId) external nonReentrant {
require(isWithdrawalPaused == false, "Withdrawal is paused");
require(
stakeFlagByTxId[internalId].holdWithdrawl == false,
"This internalId is flagged please contact admin"
);
require(
isStakeUnlocked(internalId) == true,
"Given Stake is not available for withdrawal yet"
);
// Retrieve Stake
Stake memory stake = userStakesByTxId[internalId];
// Check if already redeemed
require(stake.isRedemed == false, "Stake is already withdrawn");
(
address _tokenAddress,
uint256 _amount,
uint256 amountAfterPenalty,
uint256 penaltyAmount,
uint256 depositFee,
uint256 amountToTransfer
) = validateRequiredAmount(internalId);
if (_amount > 0) {
require(SWAP_CONTRACT_ADDRESS!= address(0), "Invalid swap address");
ISwapContract(SWAP_CONTRACT_ADDRESS).transferToStake(internalId);
}
// Update Stake Status - set isRedemed to true
userStakesByTxId[internalId].isRedemed = true;
userStakesByTxId[internalId].penaltyAmount = penaltyAmount;
userStakesByTxId[internalId].redemedTimestamp = getCurrentTimestamp();
if(penaltyAmount > 0){
userStakesByTxId[internalId].penaltyPercentage = withdrawalPenaltyInPercentage;
}
// Transfer unstaked funds to contract
if(amountToTransfer > 0){
transferAssetForWithdrawal(stake.assetAddress,address(this),stake.senderAddress,amountToTransfer);
}
bytes memory messageBytes = abi.encode(
internalId,
stake.senderAddress,
stake.assetAddress, // 0x0 in case of Native
stake.rewardAddress,
NATIVE_SYMBOL,
stake.assetSymbol,
stake.assetDecimal,
stake.apr,
stake.amount,
amountToTransfer,
penaltyAmount,
stake.durationInDays,
withdrawalPenaltyInPercentage,
stake.assetAddress == NATIVE_ASSET_ADDRESS ? 0 : 1, // 0 = Native , 1 - ERC-20 ,
getCurrentTimestamp()
);
emit XTalkMessageBroadcasted(messageBytes, DESTINATION_NETWORK, '');
emit StakeWithdraw(
internalId,
stake.senderAddress,
stake.assetAddress, // 0x0 in case of Native
stake.rewardAddress,
stake.assetSymbol,
stake.assetDecimal,
stake.apr,
stake.amount,
amountToTransfer,
penaltyAmount,
stake.durationInDays,
withdrawalPenaltyInPercentage,
stake.assetAddress == NATIVE_ASSET_ADDRESS ? 0 : 1, // 0 = Native , 1 - ERC-20 ,
getCurrentTimestamp()
);
}
function validateRequiredAmount(string memory internalId) public view returns (
address,
uint256,
uint256,
uint256,
uint256,
uint256
) {
address _tokenAddress = userStakesByTxId[internalId].assetAddress;
uint256 _stakeAmount = userStakesByTxId[internalId].amount;
uint256 _depositFeePercent = userStakesByTxId[internalId].depositFeePercent;
bool _isRedeemed = userStakesByTxId[internalId].isRedemed;
require(_isRedeemed == false, "Stake is already withdrawn");
(
uint256 amountAfterPenalty,
uint256 penaltyAmount
) = calculateAmountLeftAfterPenalty(internalId);
uint256 _depositFee = calulatePercentAmount(_depositFeePercent, _stakeAmount);
uint256 _requiredAmount = amountAfterPenalty - _depositFee;
require(_requiredAmount > 0, "Invalid internal id");
uint256 _balance;
if (_tokenAddress == NATIVE_ASSET_ADDRESS) {
// Check native balance (ETH)
_balance = address(this).balance;
} else {
// Check ERC20 token balance
IERC20 token = IERC20(_tokenAddress);
_balance = token.balanceOf(address(this));
}
uint256 _requiredSwapBalance;
if (_balance >= _requiredAmount) {
_requiredSwapBalance = 0;
} else {
uint256 swapContractBalance;
if (_tokenAddress == NATIVE_ASSET_ADDRESS) {
// Check native balance (ETH) of SWAP_CONTRACT_ADDRESS
swapContractBalance = SWAP_CONTRACT_ADDRESS.balance;
} else {
// Check ERC20 token balance of SWAP_CONTRACT_ADDRESS
IERC20 token = IERC20(_tokenAddress);
swapContractBalance = token.balanceOf(SWAP_CONTRACT_ADDRESS);
}
_requiredSwapBalance = _requiredAmount - _balance;
require(swapContractBalance >= _requiredSwapBalance, "Insufficient liquidity.");
}
return (
_tokenAddress,
_requiredSwapBalance,
amountAfterPenalty,
penaltyAmount,
_depositFee,
_requiredAmount
);
}
// User can calculate amount after subtracting penalty
function calculateAmountLeftAfterPenalty(
string memory internalId
) public view returns (uint256, uint256) {
Stake memory stake = userStakesByTxId[internalId];
require(stake.startDate > 0, "Invalid Start Date");
require(stake.isRedemed == false, "Already Unstaked the amount");
uint256 penaltyAmount = 0;
uint256 elapsedTime = getCurrentTimestamp() - stake.startDate;
if (stake.durationInDays == 0 && elapsedTime < withdrawalPenaltyInDays * (1 days)) {
penaltyAmount = calulatePercentAmount(
withdrawalPenaltyInPercentage,
stake.amount
);
}
uint256 amountLeft = (stake.amount) - penaltyAmount;
return (amountLeft, penaltyAmount);
}
// User can check if stake is unlocked
function isStakeUnlocked(
string memory internalId
) public view returns (bool) {
Stake memory stake = userStakesByTxId[internalId];
if (stake.startDate == 0) {
return false;
}
uint256 elapsedTime = getCurrentTimestamp() - stake.startDate;
if (elapsedTime >= stake.durationInDays * (1 days)) {
return true;
}
return false;
}
// Internal Function to transfer asset ERC20 or Native
function transferAssetForWithdrawal(
address assetAddress,
address from,
address to,
uint256 amount
) internal returns (bool) {
if (assetAddress == NATIVE_ASSET_ADDRESS) {
require(from.balance >= amount, "Insufficient Native balance");
// Send Native to Treasury with reentrancy Protection for Native
(bool sent, ) = to.call{value: amount}("");
return sent;
} else {
require(IERC20(assetAddress).balanceOf(from) >= amount, "Insufficient ERC20 balance");
// Send Native to Treasury with reentrancy Protection for ERC20
IERC20(assetAddress).safeTransfer(to, amount);
return true;
}
}
// Internal function to transfer asset ERC20 or Native
function transferAsset(
address assetAddress,
address from,
address to,
uint256 amount,
uint256 nativeAmount
) internal returns (bool) {
if (assetAddress == NATIVE_ASSET_ADDRESS) {
// Ensure the correct amount of native asset is sent
require(nativeAmount >= amount, "Incorrect amount of native asset sent");
// Send native asset to the recipient
(bool sent, ) = to.call{value: nativeAmount}("");
require(sent, "Failed to send native asset");
return true;
} else {
// ERC20 asset transfer
IERC20 token = IERC20(assetAddress);
require(token.balanceOf(from) >= amount, "Insufficient ERC20 balance");
require(token.allowance(from, address(this)) >= amount, "Allowance is not enough");
// Use SafeERC20 to handle transfer
token.safeTransferFrom(from, to, amount);
return true;
}
}
// Internal function to any percent amount
function calulatePercentAmount(
uint256 percent,
uint256 amount
) internal pure returns (uint256) {
if (percent > 0) {
return (amount* (percent))/ 100;
}
return 0;
}
// Internal Function for String Compare
function isEmptyString(
string memory refString
) internal pure returns (bool) {
return bytes(refString).length == 0;
}
function getCurrentTimestamp() internal view returns (uint256) {
return block.timestamp;
}
fallback() external{}
receive() external payable {}
}