Description:
Decentralized Finance (DeFi) protocol contract providing Liquidity, Staking, Factory functionality.
Blockchain: Ethereum
Source Code: View Code On The Blockchain
Solidity Source Code:
{{
"language": "Solidity",
"sources": {
"contracts/BuxJuicyCarrotStaking.sol": {
"content": "// SPDX-License-Identifier: MIT\r
pragma solidity ^0.8.24;\r
\r
/**\r
* @title QuarterLockStaking\r
* @notice Fixed 3-month staking with a default 20% APY (i.e., 5% per 3 months).\r
* - Emergency withdraw anytime with a 10% penalty (no rewards). Penalty stays in the pool.\r
* - Claim principal + rewards only after 3 months.\r
* - Adding more resets the 3-month timer and snapshots the then-current rate. Any partial,\r
* not-yet-matured reward progress is forfeited by design (explicit).\r
* - Owner can change APY (in basis points). Existing stakes keep their snapshot rate.\r
*\r
* SECURITY NOTES:\r
* - Uses ReentrancyGuard and SafeERC20.\r
* - One active stake per user to keep logic simple and predictable.\r
* - Rewards are paid from the same ERC20 token, funded into this contract by the owner.\r
* - Contract tracks liabilities (principal + snapshotted rewards) and only allows owner to\r
* recover surplus above these liabilities, preventing accidental underfunding.\r
*/\r
\r
interface IERC20 {\r
function totalSupply() external view returns (uint256);\r
function balanceOf(address) external view returns (uint256);\r
function allowance(address owner, address spender) external view returns (uint256);\r
function approve(address spender, uint256 value) external returns (bool);\r
function transfer(address to, uint256 value) external returns (bool);\r
function transferFrom(address from,address to,uint256 value) external returns (bool);\r
event Transfer(address indexed from, address indexed to, uint256 value);\r
event Approval(address indexed owner, address indexed spender, uint256 value);\r
}\r
\r
library SafeERC20 {\r
function safeTransfer(IERC20 token, address to, uint256 value) internal {\r
bool ok = token.transfer(to, value);\r
require(ok, "SafeERC20: transfer failed");\r
}\r
function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal {\r
bool ok = token.transferFrom(from, to, value);\r
require(ok, "SafeERC20: transferFrom failed");\r
}\r
function safeApprove(IERC20 token, address spender, uint256 value) internal {\r
bool ok = token.approve(spender, value);\r
require(ok, "SafeERC20: approve failed");\r
}\r
}\r
\r
abstract contract ReentrancyGuard {\r
uint256 private _status;\r
constructor() { _status = 1; }\r
modifier nonReentrant() {\r
require(_status == 1, "ReentrancyGuard: reentrant");\r
_status = 2;\r
_;\r
_status = 1;\r
}\r
}\r
\r
abstract contract Ownable {\r
address public owner;\r
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);\r
constructor() { owner = msg.sender; emit OwnershipTransferred(address(0), msg.sender); }\r
modifier onlyOwner() { require(msg.sender == owner, "Ownable: not owner"); _; }\r
function transferOwnership(address newOwner) external onlyOwner {\r
require(newOwner != address(0), "Ownable: zero");\r
emit OwnershipTransferred(owner, newOwner);\r
owner = newOwner;\r
}\r
}\r
\r
contract BuxJuicyCarrotStaking is Ownable, ReentrancyGuard {\r
using SafeERC20 for IERC20;\r
\r
// --- Config ---\r
uint256 public constant LOCK_DURATION = 90 days; // ~3 months\r
uint16 public constant PENALTY_BPS = 1000; // 10% penalty on emergency withdraw\r
uint16 public constant MAX_APY_BPS = 10_000; // 100% APY hard cap for safety\r
\r
IERC20 public immutable stakingToken;\r
\r
// APY in basis points (e.g., 2000 = 20% APY). 3-month period rate is APY/4 (integer division).\r
uint16 public apyBps = 2000; // default 20% APY - 5% per 3 months\r
\r
struct Stake {\r
uint256 amount; // principal\r
uint64 startTime; // stake start time\r
uint16 quarterRateBps; // snapshot of 3-month rate at (re)start time: apyBps/4\r
}\r
\r
mapping(address => Stake) public stakes;\r
\r
// --- Accounting (surplus protection) ---\r
// Tracks the sum of all user principals and the sum of all snapshotted full-cycle rewards.\r
uint256 public totalPrincipal;\r
uint256 public totalRewardsLiability;\r
\r
// --- Events ---\r
event Deposited(address indexed user, uint256 amount, uint256 newTotal, uint16 quarterRateBps, uint256 newMaturity);\r
event Claimed(address indexed user, uint256 principal, uint256 reward);\r
event EmergencyWithdraw(address indexed user, uint256 withdrawn, uint256 penaltyKept);\r
event ApyChanged(uint16 oldApyBps, uint16 newApyBps);\r
event Funded(address indexed from, uint256 amount);\r
event Recovered(address indexed to, uint256 amount);\r
\r
// --- Errors ---\r
error ZeroAmount();\r
error NotMature();\r
error NothingStaked();\r
error InsufficientPool();\r
error NoSurplus();\r
\r
constructor(IERC20 token_) {\r
require(address(token_) != address(0), "token=0");\r
stakingToken = token_;\r
}\r
\r
// --- Owner Functions ---\r
\r
/// @notice Set APY in basis points (e.g., 2000 = 20%). Existing stakes keep their snapshot rate.\r
function setApyBps(uint16 newApyBps) external onlyOwner {\r
require(newApyBps <= MAX_APY_BPS, "APY too high");\r
uint16 old = apyBps;\r
apyBps = newApyBps;\r
emit ApyChanged(old, newApyBps);\r
}\r
\r
/// @notice Fund the contract with reward/principal liquidity.\r
function fund(uint256 amount) external onlyOwner {\r
if (amount == 0) revert ZeroAmount();\r
stakingToken.safeTransferFrom(msg.sender, address(this), amount);\r
emit Funded(msg.sender, amount);\r
}\r
\r
/// @notice Recover *only* the surplus (balance above total liabilities).\r
function recover(uint256 amount, address to) external onlyOwner {\r
require(to != address(0), "to=0");\r
uint256 minRequired = totalPrincipal + totalRewardsLiability;\r
uint256 bal = stakingToken.balanceOf(address(this));\r
uint256 available = (bal > minRequired) ? (bal - minRequired) : 0;\r
if (available == 0 || amount > available) revert NoSurplus();\r
stakingToken.safeTransfer(to, amount);\r
emit Recovered(to, amount);\r
}\r
\r
// --- User Actions ---\r
\r
/**\r
* @notice Deposit tokens to start/reset a 3-month stake.\r
* @dev Adding more resets the timer and snapshots the *current* 3-month rate (apyBps/4).\r
* Any partial, not-yet-matured reward from the previous period is forfeited by design.\r
*/\r
function deposit(uint256 amount) external nonReentrant {\r
if (amount == 0) revert ZeroAmount();\r
\r
Stake storage s = stakes[msg.sender];\r
\r
// Remove old reward liability if there was an active stake\r
if (s.amount != 0) {\r
totalRewardsLiability -= _userLiability(s);\r
}\r
\r
// Pull tokens in\r
stakingToken.safeTransferFrom(msg.sender, address(this), amount);\r
\r
// Track principal delta\r
totalPrincipal += amount;\r
\r
// Increase principal and reset timer/rate snapshot\r
s.amount += amount;\r
s.startTime = uint64(block.timestamp);\r
s.quarterRateBps = _currentQuarterRateBps();\r
\r
// Add new reward liability for full cycle at snapshotted rate\r
totalRewardsLiability += _userLiability(s);\r
\r
emit Deposited(\r
msg.sender,\r
amount,\r
s.amount,\r
s.quarterRateBps,\r
uint256(s.startTime) + LOCK_DURATION\r
);\r
}\r
\r
/// @notice Claim principal + fixed 3-month reward after maturity.\r
function claim() external nonReentrant {\r
Stake storage s = stakes[msg.sender];\r
if (s.amount == 0) revert NothingStaked();\r
if (!_isMature(s.startTime)) revert NotMature();\r
\r
uint256 principal = s.amount;\r
uint256 reward = _userLiability(s);\r
uint256 totalOut = principal + reward;\r
\r
// Ensure pool has enough\r
if (stakingToken.balanceOf(address(this)) < totalOut) revert InsufficientPool();\r
\r
// Adjust totals before zeroing\r
totalPrincipal -= principal;\r
totalRewardsLiability -= reward;\r
\r
// Reset stake before external call\r
s.amount = 0;\r
s.startTime = 0;\r
s.quarterRateBps = 0;\r
\r
stakingToken.safeTransfer(msg.sender, totalOut);\r
emit Claimed(msg.sender, principal, reward);\r
}\r
\r
/**\r
* @notice Emergency withdraw principal at any time with a 10% penalty. No rewards.\r
* @dev Penalty stays in the pool (contract balance) to benefit remaining stakers.\r
*/\r
function emergencyWithdraw() external nonReentrant {\r
Stake storage s = stakes[msg.sender];\r
if (s.amount == 0) revert NothingStaked();\r
\r
uint256 principal = s.amount;\r
uint256 penalty = (principal * PENALTY_BPS) / 10_000;\r
uint256 payout = principal - penalty;\r
\r
// Adjust totals (remove principal and the snapshotted reward liability)\r
totalPrincipal -= principal;\r
totalRewardsLiability -= _userLiability(s);\r
\r
// Reset before transfer\r
s.amount = 0;\r
s.startTime = 0;\r
s.quarterRateBps = 0;\r
\r
stakingToken.safeTransfer(msg.sender, payout);\r
// penalty remains in contract balance\r
emit EmergencyWithdraw(msg.sender, payout, penalty);\r
}\r
\r
// --- Views ---\r
\r
/// @notice Returns current 3-month rate in bps based on APY (APY/4).\r
function currentQuarterRateBps() external view returns (uint16) {\r
return _currentQuarterRateBps();\r
}\r
\r
/// @notice Returns the maturity timestamp for the caller's current stake (0 if none).\r
function maturityOf(address user) external view returns (uint256) {\r
Stake memory s = stakes[user];\r
if (s.amount == 0) return 0;\r
return uint256(s.startTime) + LOCK_DURATION;\r
}\r
\r
/// @notice Returns pending reward if already mature; otherwise 0.\r
function pendingReward(address user) external view returns (uint256) {\r
Stake memory s = stakes[user];\r
if (s.amount == 0) return 0;\r
if (!_isMature(s.startTime)) return 0;\r
return _userLiability(s);\r
}\r
\r
/// @notice Helper to see if a stake is mature.\r
function isMature(address user) external view returns (bool) {\r
Stake memory s = stakes[user];\r
if (s.amount == 0) return false;\r
return _isMature(s.startTime);\r
}\r
\r
/// @notice Total liabilities = total principals + total snapshotted rewards for active stakes.\r
function totalLiabilities() public view returns (uint256) {\r
return totalPrincipal + totalRewardsLiability;\r
}\r
\r
/// @notice Current token balance held by the contract.\r
function poolBalance() public view returns (uint256) {\r
return stakingToken.balanceOf(address(this));\r
}\r
\r
/// @notice Surplus tokens available for safe recovery.\r
function surplus() external view returns (uint256) {\r
uint256 bal = stakingToken.balanceOf(address(this));\r
uint256 liab = totalLiabilities();\r
return bal > liab ? bal - liab : 0;\r
}\r
\r
// --- Internals ---\r
\r
function _isMature(uint64 start) internal view returns (bool) {\r
if (start == 0) return false;\r
return block.timestamp >= uint256(start) + LOCK_DURATION;\r
}\r
\r
function _currentQuarterRateBps() internal view returns (uint16) {\r
// Integer division by 4; e.g. 2000 APY -> 500 bps per quarter (5%)\r
return uint16(apyBps / 4);\r
}\r
\r
function _userLiability(Stake memory s) internal pure returns (uint256) {\r
// Full-cycle reward at the user's snapshotted quarter rate.\r
return (s.amount * s.quarterRateBps) / 10_000;\r
}\r
}\r
"
}
},
"settings": {
"optimizer": {
"enabled": true,
"runs": 500
},
"evmVersion": "paris",
"outputSelection": {
"*": {
"*": [
"evm.bytecode",
"evm.deployedBytecode",
"devdoc",
"userdoc",
"metadata",
"abi"
]
}
}
}
}}
Submitted on: 2025-10-01 17:26:21
Comments
Log in to comment.
No comments yet.