Retro pixel-art steak

steak

this is a steak.

x


This is steak. fees are sent to the staking contract and released to stakers of STEAK.


tokenomics

token: steak

ticker: STEAK

token address: 0x...

supply: 1 billion

fees: 5/5


steak contract

contract address: 0x9f8cea6d2DD0AC3c2bE048d5E572190FFBE32cB0

Stake STEAK to earn a weighted share of the fees.

contract code

contract source

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;

interface ISteakToken {
    function balanceOf(address account) external view returns (uint256);
    function transfer(address to, uint256 amount) external returns (bool);
    function transferFrom(address from, address to, uint256 amount) external returns (bool);
}

/// @notice Stakes a standard, non-rebasing ERC-20 with fee-free staking transfers.
/// @dev Owner sets the token once and funds 24-hour streams. Rewards follow weighted stake over time.
contract Steaking {
    uint256 private constant SCALE = 1e18;
    uint256 public constant STREAM_DURATION = 24 hours;

    struct Position {
        uint256 amount;
        uint256 unlockAt;
    }

    address public immutable owner;
    ISteakToken public token;
    uint256 public totalStaked;
    mapping(uint8 => uint256) public totalStakedByMode;
    uint256 public totalWeight;
    uint256 public rewardPerWeight;
    uint256 public remainingRewards;
    uint256 public remainingTime;
    uint256 public queuedRewards;
    uint256 public lastUpdate;
    mapping(address => uint256) public staked;
    mapping(address => mapping(uint8 => Position)) public positions;
    mapping(address => uint256) public weight;
    mapping(address => mapping(uint8 => uint256)) public rewards;
    mapping(address => mapping(uint8 => uint256)) public rewardPerWeightPaid;
    bool private entered;

    event Staked(address indexed user, uint8 indexed mode, uint256 amount, uint256 unlockAt);
    event Withdrawn(address indexed user, uint8 indexed mode, uint256 amount);
    event Funded(address indexed sender, uint256 amount);
    event Claimed(address indexed user, uint8 indexed mode, uint256 amount);
    event TokenSet(address indexed token);

    modifier nonReentrant() {
        require(!entered, "Reentrant call");
        entered = true;
        _;
        entered = false;
    }

    constructor() {
        owner = msg.sender;
    }

    /// @notice The deployer sets the STEAK token once, permanently.
    function setToken(address steakToken) external {
        require(msg.sender == owner, "Only owner");
        require(address(token) == address(0), "Token already set");
        require(steakToken != address(this) && steakToken.code.length > 0, "Invalid token");
        token = ISteakToken(steakToken);
        emit TokenSet(steakToken);
    }

    /// @notice Owner funding starts an idle stream or queues ETH for the next 24-hour stream.
    /// @dev Funding never changes the active stream's budget or remaining duration.
    receive() external payable nonReentrant {
        require(msg.sender == owner, "Only owner");
        require(msg.value > 0, "Zero deposit");
        require(totalStaked > 0, "No stakers");
        updateStream();
        if (remainingTime == 0) {
            remainingRewards = msg.value;
            remainingTime = STREAM_DURATION;
        } else {
            queuedRewards += msg.value;
        }
        emit Funded(msg.sender, msg.value);
    }

    function earned(address user) public view returns (uint256) {
        return earnedByMode(user, 0) + earnedByMode(user, 1) + earnedByMode(user, 2);
    }

    /// @notice Claimable ETH for one mode, including rewards saved before withdrawal.
    function earnedByMode(address user, uint8 mode) public view returns (uint256) {
        (, uint256 units) = tier(mode);
        return rewards[user][mode] + positions[user][mode].amount * units
            * (currentRewardPerWeight() - rewardPerWeightPaid[user][mode]) / SCALE;
    }

    /// @notice Reward counter including time elapsed since the last transaction.
    function currentRewardPerWeight() public view returns (uint256) {
        if (totalWeight == 0) return rewardPerWeight;
        (uint256 released,,,) = previewStream();
        return rewardPerWeight + released * SCALE / totalWeight;
    }

    /// @notice Modes 0/1/2: no lock/12 hours/24 hours, with relative weights 1x/2x/3x.
    function tier(uint8 mode) public pure returns (uint256 duration, uint256 weightUnits) {
        require(mode < 3, "Invalid mode");
        return (uint256(mode) * 12 hours, uint256(mode) + 1);
    }

    /// @notice First approve this contract to spend the requested token amount.
    /// @dev Adding tokens resets the entire selected position's lock, not other modes.
    function stake(uint256 amount, uint8 mode) external nonReentrant {
        require(address(token) != address(0), "Token not set");
        (uint256 duration, uint256 weightUnits) = tier(mode);
        require(amount > 0, "Zero amount");
        updateRewards(msg.sender, mode);
        uint256 balanceBefore = token.balanceOf(address(this));
        require(token.transferFrom(msg.sender, address(this), amount), "Token transfer failed");
        require(token.balanceOf(address(this)) == balanceBefore + amount, "Staking must be fee-free");
        Position storage position = positions[msg.sender][mode];
        position.amount += amount;
        position.unlockAt = duration == 0 ? 0 : block.timestamp + duration;
        staked[msg.sender] += amount;
        totalStaked += amount;
        totalStakedByMode[mode] += amount;
        uint256 addedWeight = amount * weightUnits;
        weight[msg.sender] += addedWeight;
        totalWeight += addedWeight;
        emit Staked(msg.sender, mode, amount, position.unlockAt);
    }

    /// @notice Withdraw tokens without losing previously earned ETH.
    /// @dev Expired positions keep their tier weight until withdrawn; no automatic renewal.
    function withdraw(uint256 amount, uint8 mode) external nonReentrant {
        (, uint256 weightUnits) = tier(mode);
        require(amount > 0, "Zero amount");
        Position storage position = positions[msg.sender][mode];
        require(amount <= position.amount, "Insufficient stake");
        require(block.timestamp >= position.unlockAt, "Stake locked");
        updateRewards(msg.sender, mode);
        position.amount -= amount;
        if (position.amount == 0) position.unlockAt = 0;
        staked[msg.sender] -= amount;
        totalStaked -= amount;
        totalStakedByMode[mode] -= amount;
        uint256 removedWeight = amount * weightUnits;
        weight[msg.sender] -= removedWeight;
        totalWeight -= removedWeight;
        require(token.transfer(msg.sender, amount), "Token transfer failed");
        emit Withdrawn(msg.sender, mode, amount);
    }

    function claim(uint8 mode) external nonReentrant {
        updateRewards(msg.sender, mode);
        uint256 amount = rewards[msg.sender][mode];
        require(amount > 0, "Nothing to claim");
        rewards[msg.sender][mode] = 0;
        (bool success,) = payable(msg.sender).call{value: amount}("");
        require(success, "ETH transfer failed");
        emit Claimed(msg.sender, mode, amount);
    }

    function updateRewards(address user, uint8 mode) private {
        updateStream();
        rewards[user][mode] = earnedByMode(user, mode);
        rewardPerWeightPaid[user][mode] = rewardPerWeight;
    }

    /// @dev Settle elapsed rewards before changing any weight. Empty pools pause the clock.
    function updateStream() private {
        (uint256 released, uint256 budget, uint256 duration, uint256 queued) = previewStream();
        if (totalWeight > 0) {
            rewardPerWeight += released * SCALE / totalWeight;
        }
        remainingRewards = budget;
        remainingTime = duration;
        queuedRewards = queued;
        lastUpdate = block.timestamp;
    }

    /// @dev At most two streams can exist: active and queued. No user or epoch loops.
    function previewStream() private view returns (
        uint256 released, uint256 budget, uint256 duration, uint256 queued
    ) {
        budget = remainingRewards;
        duration = remainingTime;
        queued = queuedRewards;
        if (totalWeight == 0 || duration == 0) return (0, budget, duration, queued);

        uint256 elapsed = block.timestamp - lastUpdate;
        uint256 used = elapsed < duration ? elapsed : duration;
        released = budget * used / duration;
        budget -= released;
        duration -= used;
        elapsed -= used;

        if (duration == 0 && queued > 0) {
            budget = queued;
            duration = STREAM_DURATION;
            queued = 0;
            used = elapsed < duration ? elapsed : duration;
            uint256 nextReleased = budget * used / duration;
            released += nextReleased;
            budget -= nextReleased;
            duration -= used;
        }
    }
}

each function explained

constructor()
Runs at deployment with no arguments. Makes the deployer the owner; the token is initially unset.
setToken(steakToken)
Only the owner can set the STEAK token, once after deployment. The address must contain contract code and cannot be this staking contract. Once set, it cannot change. Staking is blocked until setup is complete.
receive()
Only the owner can send ETH with empty transaction data. Starts a 24-hour stream if idle; otherwise queues the new ETH for the next stream. Never postpones existing rewards. Rejects zero deposits and funding with no stakers.
earned(user)
Returns the sum of the user's three mode balances.
Shows ETH earned up to the current block timestamp, in wei. Includes streamed rewards not yet recorded by a transaction; does not include future rewards or move funds.
earnedByMode(user, mode)
Reads one tier's claimable balance, including earned ETH kept after withdrawal.
currentRewardPerWeight()
Shows the reward counter including elapsed streaming time. Rewards are proportional to stake weight while time passes, not to a snapshot at funding.
tier(mode)
Returns the lock duration and multiplier: mode 0 = no lock and 1x, mode 1 = 12 hours and 2x, mode 2 = 24 hours and 3x. Other modes are rejected.
stake(amount, mode)
Deposits your STEAK into a tier after you approve the contract to spend it. Saves earlier rewards, checks that the full token amount arrived, and adds your reward weight. Adding tokens restarts the lock for your entire position in that tier only.
withdraw(amount, mode)
Returns some or all of your tokens once that tier's lock has expired. Removes only the withdrawn tokens' weight and preserves earned ETH. Tokens left staked keep their multiplier and unlock time.
claim(mode)
Sends only the selected mode's earned ETH to your wallet, even while locked or after withdrawal. Other modes keep their rewards. Failed transfers preserve the claim.
updateRewards(user, mode)
Settles the stream, saves the selected mode's earned ETH, and updates only that mode's checkpoint. Users cannot call it directly.
updateStream()
Records elapsed rewards before any weight changes, including automatic rollover into queued rewards. Pauses while no tokens are staked. No user loops or scheduled transactions are needed.
previewStream()
Calculates release through the active and queued streams without changing storage. Queued ETH starts its own 24-hour release at the active stream's end, even if no transaction happens at that moment.
nonReentrant (modifier)
A guard, not a user function. Prevents external token or ETH transfers from calling back into protected actions while one is already running.
balanceOf(account), transfer(to, amount), transferFrom(from, to, amount)
Functions on the STEAK token, declared by the interface at the top. They check token balances, return withdrawn tokens, and collect approved deposits. Staking transfers must be fee-free in both directions.
public getters
owner() shows the deployer, who sets the token once and funds reward streams. The owner cannot withdraw users' funds or postpone existing rewards with top-ups. Streams still pause if nobody is staking.
STREAM_DURATION() is 86,400 seconds. remainingRewards(), remainingTime(), queuedRewards(), and lastUpdate() show stream state at its last update, not a live countdown. Use earned(user) for currently claimable rewards.
totalStakedByMode(mode) shows all users' tokens currently staked in that mode, including unlocked tokens that have not been withdrawn.
Solidity automatically provides read-only functions for token, totalStaked, totalWeight, rewardPerWeight, staked(user), positions(user, mode), weight(user), rewards(user, mode), and rewardPerWeightPaid(user, mode). These show the token address, balances, weights, lock times, and reward bookkeeping; use earnedByMode(user, mode) for one mode or earned(user) for the total claimable ETH amount.

steak

current rewards in contract: — ETH

no lockup

1x reward weight

total staked: — STEAK

12 hour lockup

2x reward weight

total staked: — STEAK

24 hour lockup

3x reward weight

total staked: — STEAK