D
DPCDIGITAL PUBLIC COIN
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 Source
contracts/DPCPresaleStaking.solDPC 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);
}
}