آنچه در این مقاله میخوانید [پنهانسازی]
اگر می خواهید یک حراج قابل اعتماد روی بلاکچین پیاده کنید، Auction Contract با سالیدیتی نقطه شروع عملی است. در این راهنما از تعریف نیازمندی ها تا طراحی، کدنویسی، تست و دیپلوی روی شبکه تست همه مراحل را به صورت گام به گام مرور می کنیم تا در پایان یک قرارداد حراج ایمن، قابل توسعه و آماده استفاده داشته باشید.
مفاهیم پایه حراج روی بلاکچین
حراج در بلاکچین یعنی ثبت شفاف پیشنهادها، قفل شدن وجه شرکت کنندگان و تسویه خودکار بدون نیاز به اعتماد. نقش ها شامل فروشنده، شرکت کنندگان و برنده نهایی هستند. زمان بندی، قیمت پایه، حداقل افزایش قیمت و نحوه بازپرداخت به بازندگان از مهم ترین تنظیمات اولیه است.
دو الگوی رایج حراج، انگلیسی و هلندی هستند. در حراج انگلیسی قیمت از مقدار پایه شروع می شود و با پیشنهادهای بالاتر افزایش می یابد. در حراج هلندی قیمت از بالا شروع می شود و تا زمانی که کسی قبول کند کاهش می یابد. جدول زیر مقایسه سریع این دو الگو را نشان می دهد.
| نوع حراج | ویژگی های کلیدی |
|---|---|
| انگلیسی | افزایش تدریجی قیمت، برنده کسی است که بالاترین پیشنهاد را تا پایان زمان بدهد |
| هلندی | کاهش قیمت در طول زمان، اولین قبول کننده قیمت جاری برنده می شود |
در این مقاله یک حراج انگلیسی استاندارد را پیاده سازی می کنیم، چون برای فروش آیتم های واحد رایج تر است و منطق ساده تری برای شروع دارد.
طراحی معماری قرارداد حراج
پیش از کدنویسی، اجزای حالت و رفتار را مشخص کنید. به صورت معمول به آدرس فروشنده، زمان شروع و پایان، حداقل افزایش قیمت، قیمت پایه، پیشنهاد برتر فعلی و نگهداری سپرده های بازندگان نیاز دارید. همچنین رویدادها برای ثبت لحظات کلیدی مثل ثبت پیشنهاد جدید و پایان حراج ضروری هستند.
بهترین الگوی پرداخت، مدل بازپرداخت pull است؛ یعنی به جای برگرداندن وجه بازندگان در لحظه ثبت پیشنهاد جدید، مبلغ آن ها در یک نگاشت ذخیره می شود و خودشان با فراخوانی تابع withdraw آن را دریافت می کنند. این کار ریسک حمله بازدرآمد را کاهش می دهد و تراکنش ها را قابل پیش بینی تر می کند.
- متغیرهای حالت: seller، startTime، endTime، minIncrement، reservePrice، highestBidder، highestBid، ended
- الگوی بازپرداخت: mapping از آدرس به موجودی معوق برای withdraw
- رویدادها: ثبت پیشنهاد، تمدید حراج، پایان حراج، برداشت بازپرداخت
- محدودکننده ها: onlySeller برای عملیات فروشنده و auctionActive برای کنترل زمان و وضعیت
- خط مشی ضد اسنایپ: تمدید خودکار چند دقیقه در صورت پیشنهاد در لحظات پایانی
- خطوط قرمز امنیتی: عدم ارسال وجه به صورت مستقیم به بازندگان، صفر کردن بدهی قبل از انتقال
پیاده سازی قرارداد با Solidity
در این بخش یک Auction Contract با سالیدیتی به سبک حراج انگلیسی را پیاده سازی می کنیم. این نسخه شامل قیمت پایه، حداقل افزایش قیمت، تمدید ضد اسنایپ، الگوی بازپرداخت pull و متد پایان دهی امن است.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract EnglishAuction {
address payable public seller;
uint public startTime;
uint public endTime;
uint public minIncrement;
uint public reservePrice;
address public highestBidder;
uint public highestBid;
mapping(address => uint) public pendingReturns;
bool public ended;
event BidPlaced(address indexed bidder, uint amount);
event AuctionExtended(uint newEndTime);
event AuctionEnded(address winner, uint amount);
event RefundWithdrawn(address indexed bidder, uint amount);
modifier onlySeller() {
require(msg.sender == seller, "only seller");
_;
}
modifier auctionActive() {
require(block.timestamp < endTime, "auction ended");
require(!ended, "already finalized");
_;
}
constructor(
uint _biddingTime,
uint _minIncrement,
uint _reservePrice
) {
seller = payable(msg.sender);
startTime = block.timestamp;
endTime = block.timestamp + _biddingTime;
minIncrement = _minIncrement;
reservePrice = _reservePrice;
}
function bid() external payable auctionActive {
require(msg.value > 0, "no value");
uint minRequired = highestBid == 0 ? reservePrice : highestBid + minIncrement;
require(msg.value >= minRequired, "bid too low");
if (highestBidder != address(0)) {
pendingReturns[highestBidder] += highestBid;
}
highestBidder = msg.sender;
highestBid = msg.value;
emit BidPlaced(msg.sender, msg.value);
// ضد اسنایپ: اگر پیشنهاد در 2 دقیقه پایانی ثبت شد، 2 دقیقه تمدید کن
if (endTime > block.timestamp && endTime - block.timestamp <= 120) {
endTime += 120;
emit AuctionExtended(endTime);
}
}
function withdraw() external {
uint amount = pendingReturns[msg.sender];
require(amount > 0, "nothing to withdraw");
pendingReturns[msg.sender] = 0;
(bool ok, ) = payable(msg.sender).call{value: amount}("");
require(ok, "withdraw failed");
emit RefundWithdrawn(msg.sender, amount);
}
function endAuction() external onlySeller {
require(!ended, "already ended");
require(block.timestamp >= endTime, "too early");
ended = true;
emit AuctionEnded(highestBidder, highestBid);
if (highestBid > 0) {
(bool ok, ) = seller.call{value: highestBid}("");
require(ok, "payout failed");
}
}
}
مراحل راه اندازی و تست با Hardhat
برای اطمینان از صحت منطق و آماده سازی برای دیپلوی، یک محیط محلی با Hardhat بسازید و تست های پایه را اجرا کنید. مراحل زیر ترتیب پیشنهادی برای شروع سریع است.
- نصب ابزارها: Node.js را نصب کنید و یک پوشه پروژه بسازید. سپس npm init -y و بعد npm i -D hardhat اجرا کنید.
- ساخت اسکلت: npx hardhat را اجرا کنید و پروژه جاوااسکریپتی بسازید. پوشه contracts را ایجاد کنید و فایل EnglishAuction.sol را قرار دهید.
- پیکربندی شبکه: در hardhat.config تنظیم شبکه محلی و در صورت نیاز شبکه تست مثل sepolia را اضافه کنید.
- نوشتن تست: با استفاده از ethers و chai سناریوهای ثبت پیشنهاد، بازپرداخت و پایان حراج را پوشش دهید.
- اجرای تست: npx hardhat test را اجرا کنید تا منطق اصلی تایید شود.
- اسکریپت دیپلوی: در پوشه scripts یک فایل deploy.js بسازید و پارامترها را بر اساس نیاز تعیین کنید.
- دیپلوی روی شبکه تست: کلید خصوصی و RPC را به صورت امن از طریق متغیرهای محیطی تنظیم و npx hardhat run scripts/deploy.js –network sepolia را اجرا کنید.
// scripts/deploy.js
const hre = require("hardhat");
async function main() {
const Auction = await hre.ethers.getContractFactory("EnglishAuction");
const auction = await Auction.deploy(
3600, // طول حراج بر حسب ثانیه
hre.ethers.parseEther("0.01"), // حداقل افزایش
hre.ethers.parseEther("0.1") // قیمت پایه
);
await auction.waitForDeployment();
console.log("Auction deployed to:", await auction.getAddress());
}
main().catch((err) => {
console.error(err);
process.exit(1);
});
نکات امنیتی و بهترین روش ها
امنیت در قراردادهای مرتبط با وجوه واقعی غیر قابل مذاکره است. موارد زیر احتمال بروز خطا یا سوء استفاده را کاهش می دهد.
- استفاده از الگوی بازپرداخت pull و صفر کردن بدهی قبل از انتقال وجه.
- پرهیز از ارسال خودکار وجه به پیشنهاددهنده قبلی در تابع bid.
- بررسی دقیق شرط های require برای زمان، حداقل افزایش قیمت و قیمت پایه.
- قفل کردن پایان حراج با فلگ ended و انتشار رویداد برای شفافیت.
- محدود کردن endAuction به فروشنده و بعد از اتمام زمان.
- نوشتن تست برای سناریوهای لبه مثل پیشنهاد همزمان، عدم وجود برنده و تلاش برای برداشت دوباره.
خطاهای رایج و دیباگ موثر
بخش زیادی از زمان توسعه صرف رفع خطا می شود. با شناخت اشتباهات متداول، چرخه اصلاح را کوتاه کنید.
- فراموشی افزایش pendingReturns برای پیشنهاددهنده قبلی و از دست رفتن وجه او.
- استفاده از transfer با محدودیت گس. ترجیحا از call استفاده کنید و نتیجه را بررسی کنید.
- نداشتن رویدادهای کافی که روند حراج را در لاگ ها قابل پیگیری کند.
- عدم پوشش تست برای سناریوی بدون پیشنهاد و توزیع نادرست وجوه.
- قرار دادن منطق تمدید در خارج از شرط های مناسب که منجر به تمدید بیش از حد شود.
بهینه سازی گس و نگهداری
بهینه بودن مصرف گس علاوه بر کاهش هزینه کاربران، مقیاس پذیری پروژه را بهتر می کند. با بهبود کوچک در ساختار داده و منطق، صرفه جویی قابل توجهی حاصل می شود.
- استفاده از uint بجای انواع پرهزینه تر و نگه داشتن متغیرها در محدوده لازم.
- چینش متغیرهای حالت با اندازه های مشابه برای کاهش پدینگ در اسلات ها.
- اجتناب از محاسبات تکراری با ذخیره نتایج زودهنگام در حافظه محلی.
- کاهش تعداد نوشتن در حالت با به تعویق انداختن به لحظات ضروری.
- قرار دادن رویدادها فقط برای لحظات مهم تا از لاگ های زائد جلوگیری شود.
جمع بندی
با تعریف دقیق نیازمندی ها، طراحی ساده و ایمن، استفاده از الگوی بازپرداخت pull و پوشش تست های کلیدی، می توانید یک حراج شفاف و مقاوم در برابر خطا بسازید. قرارداد نمونه ارائه شده پایه مستحکمی برای افزودن قابلیت های بعدی مثل کارمزد پلتفرم، لیست سفید شرکت کنندگان، چند کالا یا زمان بندی منعطف است. با همین اصول و ابزارها، ساخت Auction Contract با سالیدیتی به یک فرایند قابل کنترل و حرفه ای تبدیل می شود.






