DPC SMART CONTRACT

Presale & Staking Contract

This Solidity contract presents the planned on-chain logic for the DPC presale with separate normal purchase and buy-and-stake flows.

Contract Sourcecontracts/DPCPresaleStaking.sol

DPC is not minted as an ERC-20 in this contract. After launch, the contract can be funded with native DPC liquidity so claims can be paid out.

SHA-256 Source Checksum

6a6bd0bf13444bf0f3194665cb1d5fd745cb40df7b3a765fc389b62312c36dbc

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

import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/utils/Pausable.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";

/// @title DPCPresaleStaking
/// @notice Presale accounting contract for Digital Public Coin (DPC), the native coin of DPC Chain.
/// @dev DPC is not minted here as an ERC-20. Buyers receive a native DPC claim after claims are enabled.
contract DPCPresaleStaking is Ownable, Pausable, ReentrancyGuard {
    using SafeERC20 for IERC20;

    uint256 public constant BPS = 10_000;
    uint256 public constant TOTAL_SUPPLY_DPC = 5_000_000_000 ether;
    uint256 public constant PRESALE_SUPPLY_DPC = 1_000_000_000 ether;
    uint256 public constant STAGE_COUNT = 50;
    uint256 public constant STAGE_SIZE_DPC = 20_000_000 ether;
    uint256 public constant START_PRICE_USDC = 8_000; // 0.008 USDC with 6 decimals
    uint256 public constant PRICE_STEP_USDC = 500; // 0.0005 USDC with 6 decimals
    uint256 public constant START_APY_BPS = 120_000; // 1200%
    uint256 public constant FINAL_APY_BPS = 5_000; // 50%
    uint256 public constant START_MONTHLY_REWARD_BPS = 2_380; // approx. effective monthly rate for 1200% APY
    uint256 public constant FINAL_MONTHLY_REWARD_BPS = 344; // approx. effective monthly rate for 50% APY
    uint256 public constant MONTH = 30 days;

    IERC20 public immutable usdc;
    address public treasury;
    bool public claimsEnabled;
    uint256 public referralRewardBps = 2_500;
    uint256 public soldDpc;

    mapping(uint256 => uint256) public stageSoldDpc;
    mapping(address => uint256) public purchasedDpc;
    mapping(address => uint256) public claimedDpc;
    mapping(address => uint256) public affiliateRewardsUsdc;

    struct StakingPosition {
        uint256 principalDpc;
        uint256 createdAt;
        uint256 stage;
        uint256 apyBps;
        uint256 monthlyRewardBps;
    }

    mapping(address => StakingPosition[]) private stakingPositions;

    event TreasuryUpdated(address indexed treasury);
    event ReferralRewardBpsUpdated(uint256 rewardBps);
    event PresalePurchase(
        address indexed buyer,
        uint256 usdcAmount,
        uint256 dpcAmount,
        bool staked,
        address indexed referrer,
        uint256 referralRewardUsdc
    );
    event ClaimsEnabled(bool enabled);
    event DpcClaimed(address indexed account, uint256 amount);
    event RescueERC20(address indexed token, address indexed to, uint256 amount);

    error InvalidAmount();
    error InvalidAddress();
    error PresaleSoldOut();
    error SelfReferral();
    error ClaimsClosed();
    error NothingToClaim();
    error NativeTransferFailed();

    constructor(address usdcToken, address treasuryWallet, address initialOwner) Ownable(initialOwner) {
        if (usdcToken == address(0) || treasuryWallet == address(0) || initialOwner == address(0)) {
            revert InvalidAddress();
        }

        usdc = IERC20(usdcToken);
        treasury = treasuryWallet;
    }

    receive() external payable {}

    function buy(uint256 usdcAmount, address referrer) external nonReentrant whenNotPaused {
        _purchase(usdcAmount, referrer, false);
    }

    function buyAndStake(uint256 usdcAmount, address referrer) external nonReentrant whenNotPaused {
        _purchase(usdcAmount, referrer, true);
    }

    function _purchase(uint256 usdcAmount, address referrer, bool stakePurchase) internal {
        if (usdcAmount == 0) revert InvalidAmount();
        if (referrer == msg.sender) revert SelfReferral();

        (uint256 dpcAmount, uint256 purchaseStage) = quoteDpcForUsdc(usdcAmount);
        if (dpcAmount == 0 || soldDpc + dpcAmount > PRESALE_SUPPLY_DPC) revert PresaleSoldOut();

        uint256 referralReward = 0;
        uint256 treasuryAmount = usdcAmount;

        if (referrer != address(0) && referralRewardBps > 0) {
            referralReward = (usdcAmount * referralRewardBps) / BPS;
            treasuryAmount = usdcAmount - referralReward;
            affiliateRewardsUsdc[referrer] += referralReward;
        }

        usdc.safeTransferFrom(msg.sender, treasury, treasuryAmount);

        if (referralReward > 0) {
            usdc.safeTransferFrom(msg.sender, referrer, referralReward);
        }

        _recordStageSale(dpcAmount);
        purchasedDpc[msg.sender] += dpcAmount;

        if (stakePurchase) {
            stakingPositions[msg.sender].push(
                StakingPosition({
                    principalDpc: dpcAmount,
                    createdAt: block.timestamp,
                    stage: purchaseStage,
                    apyBps: stageApyBps(purchaseStage),
                    monthlyRewardBps: stageMonthlyRewardBps(purchaseStage)
                })
            );
        }

        emit PresalePurchase(msg.sender, usdcAmount, dpcAmount, stakePurchase, referrer, referralReward);
    }

    function quoteDpcForUsdc(uint256 usdcAmount) public view returns (uint256 dpcAmount, uint256 lastStage) {
        uint256 remainingUsdc = usdcAmount;
        uint256 simulatedSold = soldDpc;

        while (remainingUsdc > 0 && simulatedSold < PRESALE_SUPPLY_DPC) {
            uint256 stage = _stageForSold(simulatedSold);
            uint256 stageSold = simulatedSold - ((stage - 1) * STAGE_SIZE_DPC);
            uint256 availableDpc = STAGE_SIZE_DPC - stageSold;
            uint256 price = stagePriceUsdc(stage);
            uint256 costToFillStage = (availableDpc * price) / 1 ether;

            lastStage = stage;

            if (remainingUsdc >= costToFillStage) {
                dpcAmount += availableDpc;
                simulatedSold += availableDpc;
                remainingUsdc -= costToFillStage;
            } else {
                uint256 partialDpc = (remainingUsdc * 1 ether) / price;
                dpcAmount += partialDpc;
                simulatedSold += partialDpc;
                remainingUsdc = 0;
            }
        }
    }

    function claimableDpc(address account) public view returns (uint256) {
        uint256 gross = purchasedDpc[account] + stakingRewardDpc(account);

        if (gross <= claimedDpc[account]) {
            return 0;
        }

        return gross - claimedDpc[account];
    }

    function claimDpc() external nonReentrant {
        if (!claimsEnabled) revert ClaimsClosed();

        uint256 amount = claimableDpc(msg.sender);
        if (amount == 0) revert NothingToClaim();

        claimedDpc[msg.sender] += amount;

        (bool success, ) = payable(msg.sender).call{value: amount}("");
        if (!success) revert NativeTransferFailed();

        emit DpcClaimed(msg.sender, amount);
    }

    function stakingRewardDpc(address account) public view returns (uint256 rewardDpc) {
        StakingPosition[] storage positions = stakingPositions[account];

        for (uint256 i = 0; i < positions.length; i++) {
            StakingPosition storage position = positions[i];
            uint256 monthsElapsed = (block.timestamp - position.createdAt) / MONTH;
            uint256 balance = position.principalDpc;

            for (uint256 monthIndex = 0; monthIndex < monthsElapsed; monthIndex++) {
                balance += (balance * position.monthlyRewardBps) / BPS;
            }

            rewardDpc += balance - position.principalDpc;
        }
    }

    function stakingPositionCount(address account) external view returns (uint256) {
        return stakingPositions[account].length;
    }

    function stakingPosition(address account, uint256 index) external view returns (StakingPosition memory) {
        return stakingPositions[account][index];
    }

    function stagePriceUsdc(uint256 stage) public pure returns (uint256) {
        require(stage >= 1 && stage <= STAGE_COUNT, "DPC: invalid stage");
        return START_PRICE_USDC + ((stage - 1) * PRICE_STEP_USDC);
    }

    function stageApyBps(uint256 stage) public pure returns (uint256) {
        require(stage >= 1 && stage <= STAGE_COUNT, "DPC: invalid stage");
        uint256 decrease = ((START_APY_BPS - FINAL_APY_BPS) * (stage - 1)) / (STAGE_COUNT - 1);
        return START_APY_BPS - decrease;
    }

    function stageMonthlyRewardBps(uint256 stage) public pure returns (uint256) {
        require(stage >= 1 && stage <= STAGE_COUNT, "DPC: invalid stage");
        uint256 decrease =
            ((START_MONTHLY_REWARD_BPS - FINAL_MONTHLY_REWARD_BPS) * (stage - 1)) / (STAGE_COUNT - 1);
        return START_MONTHLY_REWARD_BPS - decrease;
    }

    function currentStage() public view returns (uint256) {
        if (soldDpc >= PRESALE_SUPPLY_DPC) {
            return STAGE_COUNT;
        }

        return _stageForSold(soldDpc);
    }

    function _stageForSold(uint256 sold) internal pure returns (uint256) {
        return (sold / STAGE_SIZE_DPC) + 1;
    }

    function _recordStageSale(uint256 dpcAmount) internal {
        uint256 remainingDpc = dpcAmount;

        while (remainingDpc > 0) {
            uint256 stage = _stageForSold(soldDpc);
            uint256 stageSold = soldDpc - ((stage - 1) * STAGE_SIZE_DPC);
            uint256 availableDpc = STAGE_SIZE_DPC - stageSold;
            uint256 fillAmount = remainingDpc > availableDpc ? availableDpc : remainingDpc;

            stageSoldDpc[stage] += fillAmount;
            soldDpc += fillAmount;
            remainingDpc -= fillAmount;
        }
    }

    function setTreasury(address treasuryWallet) external onlyOwner {
        if (treasuryWallet == address(0)) revert InvalidAddress();
        treasury = treasuryWallet;
        emit TreasuryUpdated(treasuryWallet);
    }

    function setReferralRewardBps(uint256 rewardBps) external onlyOwner {
        require(rewardBps <= 2_500, "DPC: referral too high");
        referralRewardBps = rewardBps;
        emit ReferralRewardBpsUpdated(rewardBps);
    }

    function setClaimsEnabled(bool enabled) external onlyOwner {
        claimsEnabled = enabled;
        emit ClaimsEnabled(enabled);
    }

    function pause() external onlyOwner {
        _pause();
    }

    function unpause() external onlyOwner {
        _unpause();
    }

    function rescueERC20(address token, address to, uint256 amount) external onlyOwner {
        if (token == address(0) || to == address(0)) revert InvalidAddress();
        IERC20(token).safeTransfer(to, amount);
        emit RescueERC20(token, to, amount);
    }
}
DPC Smart Contract | Presale, Staking & Native DPC Claims