قرارداد زیر کاملاً پیچیده است، اما بسیاری از ویژگیهای سالیدیتی را به نمایش میگذارد. قرارداد رای گیری را اجرا میکند. البته، مشکلات اصلی رای گیری الکترونیکی نحوه واگذاری حق رای به افراد صحیح و نحوه جلوگیری از دستکاری است. ما همه مشکلات را در اینجا حل نخواهیم کرد، اما حداقل نشان خواهیم داد که چگونه میتوان رای گیریِ نمایندگان را انجام داد تا شمارش آراء در همان زمان به صورت خودکار و کاملاً شفاف انج
قرارداد زیر کاملاً پیچیده است، اما بسیاری از ویژگیهای سالیدیتی را به نمایش میگذارد. قرارداد رای گیری را اجرا میکند. البته، مشکلات اصلی رای گیری الکترونیکی نحوه واگذاری حق رای به افراد صحیح و نحوه جلوگیری از دستکاری است. ما همه مشکلات را در اینجا حل نخواهیم کرد، اما حداقل نشان خواهیم داد که چگونه میتوان رای گیریِ نمایندگان را انجام داد تا شمارش آراء در همان زمان به صورت خودکار و کاملاً شفاف انجام شود
ایده این است که یک قرارداد برای هر رأی ایجاد شود و نام کوتاهی برای هر گزینه ارائه شود. سپس خالق قرارداد که به عنوان رئیس فعالیت میکند به هر آدرس به طور جداگانه حق رای میدهد.
افراد پشت آدرسها میتوانند انتخاب کنند که یا خود رأی دهند یا رای خود را به شخصی که به او اعتماد دارند واگذار کنند.
در پایان زمان رای گیری، ()winningProposal پیشنهاد را با بیشترین تعداد رای برمیگرداند.
پیشنهاد ویژه: آموزش رایگان سالیدیتی
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
/// @title Voting with delegation.
contract Ballot {
// This declares a new complex type which will
// be used for variables later.
// It will represent a single voter.
struct Voter {
uint weight; // weight is accumulated by delegation
bool voted; // if true, that person already voted
address delegate; // person delegated to
uint vote; // index of the voted proposal
}
// This is a type for a single proposal.
struct Proposal {
bytes32 name; // short name (up to 32 bytes)
uint voteCount; // number of accumulated votes
}
address public chairperson;
// This declares a state variable that stores a `Voter` struct for each possible address.
mapping(address => Voter) public voters;
// A dynamically-sized array of `Proposal` structs.
Proposal[] public proposals;
/// Create a new ballot to choose one of `proposalNames`.
constructor(bytes32[] memory proposalNames) {
chairperson = msg.sender;
voters[chairperson].weight = 1;
// For each of the provided proposal names,
// create a new proposal object and add it
// to the end of the array.
for (uint i = 0; i < proposalNames.length; i++) {
proposals.push(Proposal({
name: proposalNames[i],
voteCount: 0
}));
}
}
// Give `voter` the right to vote on this ballot.
// May only be called by `chairperson`.
function giveRightToVote(address voter) external {
require(msg.sender == chairperson, "Only chairperson can give right to vote.");
require(!voters[voter].voted, "The voter already voted.");
require(voters[voter].weight == 0);
voters[voter].weight = 1;
}
/// Delegate your vote to the voter `to`.
function delegate(address to) external {
// assigns reference
Voter storage sender = voters[msg.sender];
require(sender.weight != 0, "You have no right to vote");
require(!sender.voted, "You already voted.");
require(to != msg.sender, "Self-delegation is disallowed.");
// Forward the delegation as long as
// `to` also delegated.
// In general, such loops are very dangerous,
// because if they run too long, they might
// need more gas than is available in a block.
// In this case, the delegation will not be executed,
// but in other situations, such loops might
// cause a contract to get "stuck" completely.
while (voters[to].delegate != address(0)) {
to = voters[to].delegate;
// We found a loop in the delegation, not allowed.
require(to != msg.sender, "Found loop in delegation.");
}
Voter storage delegate_ = voters[to];
// Voters cannot delegate to accounts that cannot vote.
require(delegate_.weight >= 1);
// Since `sender` is a reference, this modifies `voters[msg.sender]`.
sender.voted = true;
sender.delegate = to;
if (delegate_.voted) {
// If the delegate already voted,
// directly add to the number of votes
proposals[delegate_.vote].voteCount += sender.weight;
} else {
// If the delegate did not vote yet,
// add to her weight.
delegate_.weight += sender.weight;
}
}
/// Give your vote (including votes delegated to you)
/// to proposal `proposals[proposal].name`.
function vote(uint proposal) external {
Voter storage sender = voters[msg.sender];
require(sender.weight != 0, "Has no right to vote");
require(!sender.voted, "Already voted.");
sender.voted = true;
sender.vote = proposal;
// If `proposal` is out of the range of the array,
// this will throw automatically and revert all changes.
proposals[proposal].voteCount += sender.weight;
}
/// @dev Computes the winning proposal taking all previous votes into account.
function winningProposal() public view returns (uint winningProposal_) {
uint winningVoteCount = 0;
for (uint p = 0; p < proposals.length; p++) {
if (proposals[p].voteCount > winningVoteCount) {
winningVoteCount = proposals[p].voteCount;
winningProposal_ = p;
}
}
}
// Calls winningProposal() function to get the index
// of the winner contained in the proposals array and then
// returns the name of the winner
function winnerName() external view returns (bytes32 winnerName_) {
winnerName_ = proposals[winningProposal()].name;
}
}
در حال حاضر، بسیاری از تراکنش ها برای واگذاری حق رای به همه شرکت کنندگان مورد نیاز است. علاوه بر این، اگر دو یا چند پروپوزال دارای تعداد آرای یکسانی باشند، ()winningProposal قادر به ثبت تساوی نیست. آیا میتوانید راهی برای رفع این مشکلات فکر کنید؟
در این بخش، نشان خواهیم داد که ایجاد یک قرارداد حراجِ کاملا کور در اتریوم چقدر آسان است. ما با یک مزایده باز شروع میکنیم که در آن همه میتوانند پیشنهادات ارائه شده را ببینند و سپس این قرارداد را به حراج کور توسعه میدهیم که در آن امکان مشاهده پیشنهاد واقعی تا زمان پایان مناقصه وجود ندارد.
ایدهی کلی قرارداد مزایده ساده زیر این است که همه میتوانند پیشنهادات خود را در طول یک دوره مناقصه ارسال کنند. پیشنهادات در حال حاضر شامل ارسال پول/ اتر به منظور متصل کردن مناقصهگرانِ مناقصه به پیشنهاد آنها است. اگر بالاترین پیشنهاد افزایش یابد، مناقصهگران قبلی پول خود را پس میگیرد. پس از پایان دوره مناقصه، قرارداد باید به صورت دستی فراخوانده شود تا ذینفع پول خود را دریافت کند- قراردادها نمیتوانند خودشان فعال شوند.
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;
contract SimpleAuction {
// Parameters of the auction. Times are either
// absolute unix timestamps (seconds since 1970-01-01)
// or time periods in seconds.
address payable public beneficiary;
uint public auctionEndTime;
// Current state of the auction.
address public highestBidder;
uint public highestBid;
// Allowed withdrawals of previous bids.
mapping(address => uint) pendingReturns;
// Set to true at the end, disallows any change.
// By default initialized to `false`.
bool ended;
// Events that will be emitted on changes.
event HighestBidIncreased(address bidder, uint amount);
event AuctionEnded(address winner, uint amount);
// Errors that describe failures.
// The triple-slash comments are so-called natspec
// comments. They will be shown when the user
// is asked to confirm a transaction or
// when an error is displayed.
/// The auction has already ended.
error AuctionAlreadyEnded();
/// There is already a higher or equal bid.
error BidNotHighEnough(uint highestBid);
/// The auction has not ended yet.
error AuctionNotYetEnded();
/// The function auctionEnd has already been called.
error AuctionEndAlreadyCalled();
/// Create a simple auction with `biddingTime`
/// seconds bidding time on behalf of the
/// beneficiary address `beneficiaryAddress`.
constructor(uint biddingTime, address payable beneficiaryAddress) {
beneficiary = beneficiaryAddress;
auctionEndTime = block.timestamp + biddingTime;
}
/// Bid on the auction with the value sent
/// together with this transaction.
/// The value will only be refunded if the
/// auction is not won.
function bid() external payable {
// Revert the call if the bidding
// period is over.
if (block.timestamp > auctionEndTime)
revert AuctionAlreadyEnded();
// If the bid is not higher, send the
// money back (the revert statement
// will revert all changes in this
// function execution including
// it having received the money).
if (msg.value <= highestBid)
revert BidNotHighEnough(highestBid);
if (highestBid != 0) {
// Sending back the money by simply using
// highestBidder.send(highestBid) is a security risk
// because it could execute an untrusted contract.
// It is always safer to let the recipients
// withdraw their money themselves.
pendingReturns[highestBidder] += highestBid;
}
highestBidder = msg.sender;
highestBid = msg.value;
emit HighestBidIncreased(msg.sender, msg.value);
}
/// Withdraw a bid that was overbid.
function withdraw() external returns (bool) {
uint amount = pendingReturns[msg.sender];
if (amount > 0) {
// It is important to set this to zero because the recipient
// can call this function again as part of the receiving call
// before `send` returns.
pendingReturns[msg.sender] = 0;
// msg.sender is not of type `address payable` and must be
// explicitly converted using `payable(msg.sender)` in order
// to use the member function `send()`.
if (!payable(msg.sender).send(amount)) {
// No need to call throw here, just reset the amount owing
pendingReturns[msg.sender] = amount;
return false;
}
}
return true;
}
/// End the auction and send the highest bid
/// to the beneficiary.
function auctionEnd() external {
// 1. Conditions
if (block.timestamp < auctionEndTime)
revert AuctionNotYetEnded();
if (ended)
revert AuctionEndAlreadyCalled();
// 2. Effects
ended = true;
emit AuctionEnded(highestBidder, highestBid);
// 3. Interaction
beneficiary.transfer(highestBid);
}
}
مزایده باز قبلی در ادامه به مزایده کور توسعه یافتهاست. مزیت مزایده کور این است که هیچ گونه فشار زمانی نسبت به پایان دوره مناقصه وجود ندارد. ایجاد مزایده کور بر روی یک پلتفرم محاسباتی شفاف ممکن است متناقض به نظر برسد، اما رمزنگاری به کمک شما میآید.
در طول دوره مناقصه، یک پیشنهاد دهنده در واقع پیشنهاد خود را ارسال نمیکند، بلکه فقط یک نسخه هش شده از آن را ارسال میکند. از آنجا که در حال حاضر یافتن دو مقدار (به اندازه کافی طولانی) که مقادیر هش آنها برابر باشد، عملاً غیرممکن تلقی میشود، مناقصهگر با این کار متعهد به مناقصه میشود. پس از پایان دوره مناقصه، مناقصهگران باید پیشنهادات خود را آشکار کنند. آنها مقادیر خود را بدون رمزگذاری ارسال میکنند و قرارداد بررسی میکند که مقدار هش همان مقدار ارائه شده در دوره مناقصه است.
چالش دیگر این است که چگونه مزایده را به طور همزمان اجباری و پنهان یا کور جلوه دهید: تنها راه جلوگیری از ارسال نکردن مبلغ توسط داوطلب پس از برنده شدن در مزایده، ارسال آنها به همراه پیشنهاد است. از آنجا که انتقال مقدار در اتریوم پنهان یا کور نمیشود، هر کسی میتواند مقدار را ببیند.
قرارداد زیر با قبول هر مقداری که بزرگتر از بالاترین پیشنهاد باشد، این مشکل را حل میکند. از آنجایی که این فقط در مرحله آشکار شدن قابل بررسی است، ممکن است برخی از پیشنهادات نامعتبر باشند، و این هدفمند است (حتی یک فلَگ صریح برای قرار دادن پیشنهادات نامعتبر با انتقال مقدار بالا ارائه میدهد): مناقصهگران با قرار دادن چند پیشنهاد زیاد یا کم اعتبار، میتوانند رقابت را مختل کنند.
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;
contract BlindAuction {
struct Bid {
bytes32 blindedBid;
uint deposit;
}
address payable public beneficiary;
uint public biddingEnd;
uint public revealEnd;
bool public ended;
mapping(address => Bid[]) public bids;
address public highestBidder;
uint public highestBid;
// Allowed withdrawals of previous bids
mapping(address => uint) public pendingReturns;
event AuctionEnded(address winner, uint highestBid);
// Errors that describe failures.
error TooEarly(uint time);
error TooLate(uint time);
error AuctionEndAlreadyCalled();
// Modifiers
modifier onlyBefore(uint time) {
if (block.timestamp >= time) revert TooLate(time);
_;
}
modifier onlyAfter(uint time) {
if (block.timestamp <= time) revert TooEarly(time);
_;
}
constructor(
uint biddingTime,
uint revealTime,
address payable beneficiaryAddress
) {
beneficiary = beneficiaryAddress;
biddingEnd = block.timestamp + biddingTime;
revealEnd = biddingEnd + revealTime;
}
// Place a blinded bid with `blindedBid` = keccak256(abi.encodePacked(value, fake, secret)).
// The sent ether is only refunded if the bid is correctly revealed in the revealing phase.
// The bid is valid if the ether sent together with the bid is at least "value" and "fake" is not true.
// Setting "fake" to true and sending not the exact amount are ways to hide the real bid but still make the required deposit.
// The same address can place multiple bids.
function bid(bytes32 blindedBid) external payable onlyBefore(biddingEnd) {
bids[msg.sender].push(Bid({
blindedBid: blindedBid,
deposit: msg.value
}));
}
// Reveal your blinded bids. You will get a refund for all correctly blinded invalid bids and for all bids except for the totally highest.
function reveal(
uint[] calldata values,
bool[] calldata fakes,
bytes32[] calldata secrets
) external onlyAfter(biddingEnd) onlyBefore(revealEnd) {
uint length = bids[msg.sender].length;
require(values.length == length);
require(fakes.length == length);
require(secrets.length == length);
uint refund;
for (uint i = 0; i < length; i++) {
Bid storage bidToCheck = bids[msg.sender][i];
(uint value, bool fake, bytes32 secret) = (values[i], fakes[i], secrets[i]);
if (bidToCheck.blindedBid != keccak256(abi.encodePacked(value, fake, secret))) {
// Bid was not actually revealed. Do not refund deposit.
continue;
}
refund += bidToCheck.deposit;
if (!fake && bidT
خرید کالا از راه دور در حال حاضر نیاز به چندین طرف دارد که باید به یکدیگر اعتماد کنند. ساده ترین پیکربندی شامل یک فروشنده و یک خریدار است. خریدار مایل است کالایی را از فروشنده دریافت کند و فروشنده مایل است در ازای آن پول (یا معادل آن) دریافت کند. قسمت مشکل ساز محموله در اینجا است: هیچ راهی برای تعیین اطمینان از رسیدن کالا به خریدار وجود ندارد.
روشهای مختلفی برای حل این مشکل وجود دارد، اما همه آنها در مقابل یک یا بقیه راهها کم میآورند. در مثال زیر، هر دو طرف باید مقدار دو برابر یک قلم کالا را به عنوان ضمانت در قرارداد قرار دهند. به محض اینکه این اتفاق افتاد، پول در قرارداد قفل شده خواهد ماند تا زمانی که خریدار تأیید کند که کالا را دریافت کردهاست. پس از آن، مقدار (نیمی از سپرده خود) به خریدار بازگردانده میشود و فروشنده سه برابر مقدار (سپرده خود به علاوه مقدار) دریافت میکند. ایده پشت این امر این است که هر دو طرف انگیزهای برای حل اوضاع دارند یا در غیر این صورت پول آنها برای همیشه قفل شده خواهد ماند.
البته این قرارداد مشکلی را حل نمیکند، اما یک نمای کلی از چگونگی استفاده از ساختار ماشین حالت مانند در داخل قرارداد ارائه میدهد.
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;
contract Purchase {
uint public value;
address payable public seller;
address payable public buyer;
enum State { Created, Locked, Release, Inactive }
State public state;
modifier condition(bool condition_) {
require(condition_);
_;
}
error OnlyBuyer();
error OnlySeller();
error InvalidState();
error ValueNotEven();
modifier onlyBuyer() {
if (msg.sender != buyer) revert OnlyBuyer();
_;
}
modifier onlySeller() {
if (msg.sender != seller) revert OnlySeller();
_;
}
modifier inState(State state_) {
if (state != state_) revert InvalidState();
_;
}
event Aborted();
event PurchaseConfirmed();
event ItemReceived();
event SellerRefunded();
constructor() payable {
seller = payable(msg.sender);
value = msg.value / 2;
if ((2 * value) != msg.value) revert ValueNotEven();
}
// Abort the purchase and reclaim the ether.
// Can only be called by the seller before the contract is locked.
function abort() external onlySeller inState(State.Created) {
emit Aborted();
state = State.Inactive;
seller.transfer(address(this).balance);
}
// Confirm the purchase as buyer.
// Transaction has to include `2 * value` ether.
// The ether will be locked until confirmReceived is called.
function confirmPurchase() external inState(State.Created) condition(msg.value == (2 * value)) payable {
emit PurchaseConfirmed();
buyer = payable(msg.sender);
state = State.Locked;
}
// Confirm that you (the buyer) received the item.
// This will release the locked ether.
function confirmReceived() external onlyBuyer inState(State.Locked) {
emit ItemReceived();
state = State.Release;
buyer.transfer(value);
}
// This function refunds the seller, i.e. pays back the locked funds of the seller.
function refundSeller() external onlySeller inState(State.Release) {
emit SellerRefunded();
state = State.Inactive;
seller.transfer(3 * value);
}
}
در این بخش نحوه ساختن نمونه پیاده سازی کانال پرداخت را خواهیم آموخت. کانال پرداخت از امضاهای رمزنگاری شده برای انتقال مکرر اتر بین طرفهای مشابه به صورت ایمن، آنی و بدون کارمزد استفاده میکند. برای مثال، باید نحوه امضا و تأیید امضاها و راه اندازی کانال پرداخت را درک کنیم.
تصور کنید آلیس میخواهد مقداری اتر برای باب ارسال کند، یعنی آلیس فرستنده است و باب گیرنده آن میباشد.
آلیس فقط باید پیام های خارج از زنجیرهِ امضا شده با رمزنگاری (مثلاً از طریق ایمیل) را به باب بفرستد و این شبیه چک نوشتن است.
آلیس و باب برای تأیید تراکنشها از امضاها استفاده میکنند که با قراردادهای هوشمند در اتریوم امکان پذیر است. آلیس یک قرارداد هوشمند ساده خواهد ساخت که به او امکان میدهد اتر را منتقل کند، اما به جای اینکه خودش یک تابع را برای شروع پرداخت فراخوانی کند، به باب اجازه این کار را میدهد و بنابراین هزینه تراکنش را پرداخت میکند. قرارداد به شرح زیر کار میکند:
آلیس قرارداد ReceiverPays را دیپلوی میکند، به اندازه کافی اتر را برای پوشش پرداختهایی که انجام خواهد شد، پیوست میکند.
آلیس با امضای پیام با کلید خصوصی خود اجازه پرداخت را میدهد.
آلیس پیام امضا شده با رمزنگاری را برای باب میفرستد. نیازی به مخفی نگه داشتن پیام نیست (بعداً توضیح داده خواهد شد) و سازوکار ارسال آن اهمیتی ندارد.
باب با ارائه پیام امضا شده به قرارداد هوشمند، پرداخت خود را مدعی میشود. قرارداد صحت پیام را تأیید میکند و سپس وجوه را آزاد میکند.
آلیس برای امضای تراکنش نیازی به تعامل با شبکه اتریوم ندارد، روند کار کاملا آفلاین است. در این آموزش، ما با استفاده از روش توصیف شده در EIP-712 پیامها را در مرورگر با استفاده از web3.js و MetaMask امضا خواهیم کرد، زیرا تعدادی از مزایای امنیتی دیگر را فراهم میکند.
// Hashing first makes things easier
var hash = web3.utils.sha3("message to sign");
web3.eth.personal.sign(
hash,
web3.eth.defaultAccount,
function() {
console.log("Signed");
}
);
توجه داشته باشید
web3.eth.personal.sign طول پیام را به دادههای امضا شده اضافه میکند. از آنجا که ما ابتدا هش میکنیم، پیام همیشه دقیقاً 32 بایت خواهد بود و بنابراین این پیشوند طول همیشه یکسان است.
برای قراردادی که پرداختها را انجام میدهد، پیام امضا شده باید شامل موارد زیر باشد:
حمله مجدد زمانی رخ میدهد که از پیام امضا شده مجدداً برای درخواست مجوز برای اقدام دیگر استفاده میشود. برای جلوگیری از حملات مجدد، ما از همان روش تراکنش اتریوم استفاده میکنیم، اصطلاحاً نانس نامیده میشود، یعنی تعداد تراکنشهای ارسال شده توسط یک حساب میباشد. قرارداد هوشمند بررسی میکند که آیا یک نانس چندین بار استفاده شدهاست. نوع دیگر حمله مجدد میتواند هنگامی رخ دهد که مالکِ قرارداد هوشمند ReceiverPays را دیپلوی کند، مقداری پرداخت انجام بدهد و سپس قرارداد را از بین ببرد. بعداً، آنها تصمیم میگیرند که قرارداد هوشمند RecipientPays را دوباره دیپلوی کنند، اما قرارداد جدید، نانس استفاده شده در دیپلوی قبلی را نمیشناسد، بنابراین مهاجم میتواند دوباره از پیامهای قدیمی استفاده کند. آلیس میتواند با درج آدرس قرارداد در پیام در برابر این حمله محافظت کند و فقط پیامهای حاوی آدرس قرارداد خود پذیرفته میشوند. نمونهای از این مورد را میتوانید در دو خط اول تابع ()claimPayment در قرارداد کامل در انتهای این بخش بیابید.
حالا که ما مشخص کردهایم که چه اطلاعاتی را باید در پیام امضا شده قرار دهیم، ما آماده هستیم که پیام را کنار هم قرار دهیم و آن را هش و امضا کنیم. برای سادگی، دادهها را بهم پیوند میدهیم. کتابخانه ethereumjs-abi تابعی به نام soliditySHA3 را فراهم میکند که رفتار keccak256 سالیدیتی را که برای آرگومانهای رمزگذاری شده با استفاده از abi.encodePacked اعمال میشود، را تقلید میکند. در اینجا یک تابع جاوا اسکریپت وجود دارد که امضای مناسب را برای مثال ReceiverPays ایجاد میکند:
function signPayment(
recipient,
amount,
nonce,
contractAddress,
callback
) {
var hash = "0x" + abi.soliditySHA3(
[
"address",
"uint256",
"uint256",
"address"
],
[
recipient,
amount,
nonce,
contractAddress
]
).toString("hex");
web3.eth.personal.sign(
hash,
web3.eth.defaultAccount,
callback
);
}
به طور کلی، امضاهای ECDSA از دو پارامتر r و s تشکیل شده است. امضاها در اتریوم شامل پارامتر سومی به نام v هستند که میتوانید از آن برای تأیید اینکه از کلید خصوصی حساب برای امضای پیام استفاده شده است و فرستنده تراکنش استفاده کنید. Solidity یک ecrecover تابع داخلی را ارائه میدهد که پیامی را به همراه پارامترهای r ، s و v میپذیرد و آدرسی را که برای امضای پیام استفاده شده است، برمیگرداند.
امضاهای تولید شده توسط web3.js ترکیبی از r ، s و v هستند، بنابراین اولین قدم این است که این پارامترها را از هم جدا کنیم. شما می توانید این کار را در سمت مشتری انجام دهید، اما انجام آن در داخل قرارداد هوشمند به این معنی است که به جای سه پارامتر، فقط باید یک پارامتر امضا ارسال کنید. تفکیک یک آرایه بایتی به اجزای تشکیل دهنده آن مشکل است، بنابراین ما از اسمبلی درون خطی برای انجام کار در تابع splitSignature (سومین تابع در قرارداد کامل در انتهای این بخش) استفاده می کنیم.
قرارداد هوشمند باید دقیقاً بداند چه پارامترهایی امضا شدهاند، بنابراین باید پیام را از طریق پارامترها دوباره ایجاد کند و از آن برای تأیید امضا استفاده کند. توابع prefixed و recoverSigner این کار را در تابع claimPayment انجام میدهند.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract ReceiverPays {
address owner = msg.sender;
mapping(uint256 => bool) usedNonces;
constructor() payable {}
function claimPayment(
uint256 amount,
uint256 nonce,
bytes memory signature
) external {
require(!usedNonces[nonce]);
usedNonces[nonce] = true;
// this recreates the message that was signed on the client
bytes32 message = prefixed(
keccak256(
abi.encodePacked(
msg.sender,
amount,
nonce,
this
)
)
);
require(
recoverSigner(message, signature) == owner
);
payable(msg.sender).transfer(amount);
}
// destroy the contract and reclaim the leftover funds.
function shutdown() external {
require(msg.sender == owner);
selfdestruct(payable(msg.sender));
}
// signature methods.
function splitSignature(bytes memory sig)
internal
pure
returns (
uint8 v,
bytes32 r,
bytes32 s
)
{
require(sig.length == 65);
assembly {
// first 32 bytes, after the length prefix.
r := mload(add(sig, 32))
// second 32 bytes.
s := mload(add(sig, 64))
// final byte (first byte of the next 32 bytes).
v := byte(0, mload(add(sig, 96)))
}
return (v, r, s);
}
function recoverSigner(
bytes32 message,
bytes memory sig
) internal pure returns (address) {
(uint8 v, bytes32 r, bytes32 s) = splitSignature(sig);
return ecrecover(message, v, r, s);
}
// builds a prefixed hash to mimic the beha
اکنون آلیس یک پیاده سازی ساده اما کامل از یک کانال پرداخت ایجاد کردهاست. کانالهای پرداخت از امضاهای رمزنگاری شده برای انتقال مکرر اتر به صورت ایمن، فوری و تراکنش بدون کارمزد استفاده میکنند.
کانالهای پرداخت به شرکت کنندگان این امکان را میدهد تا انتقالهای مکرر اتر را بدون استفاده از تراکنش انجام دهند. این بدان معنی است که شما میتوانید از تأخیر و هزینههای مرتبط با تراکنشها جلوگیری کنید. ما میخواهیم یک کانال پرداخت ساده بین دو طرف (آلیس و باب) را بررسی کنیم. شامل سه مرحله است:
توجه داشته باشید
فقط مراحل 1 و 3 به تراکنشهای اتریوم نیاز دارند، مرحله 2 به این معنی است که فرستنده از طریق روشهای غیر زنجیرهای (به عنوان مثال ایمیل) پیام امضا شده رمزنگاری شده را به گیرنده منتقل میکند. این بدان معناست که برای پشتیبانی از هر تعداد تراکنش فقط دو تراکنش لازم است.
تضمین شده که باب وجوه خود را دریافت میکند زیرا قرارداد هوشمند اتر را اسکو میکند (اسکو به معنی اینکه طرفین معامله بر یک سری شرایط توافق میکنند که اگر این شرایط توسط دو طرفین انجام شوند طرف ثالث مانند قرارداد هوشمند امکان برداشت یا پرداخت وجوه را امکان پذیر میکند.) و قول یک پیام معتبر امضا شده را میدهد. قرارداد هوشمند همچنین مهلت زمانی را اعمال میکند، بنابراین آلیس تضمین میکند که سرانجام وجوه خود را بازیابی میکند حتی اگر گیرنده از بستن کانال خودداری کند. شرکت کنندگان در یک کانال پرداخت تصمیم میگیرند که چه مدت آن را باز نگه دارند. برای یک معامله کوتاه مدت، مانند پرداخت کافینت به ازای هر دقیقه دسترسی به شبکه، کانال پرداخت ممکن است به مدت محدودی باز نگه داشته شود. از طرف دیگر، برای پرداخت مکرر، مانند پرداخت دستمزد ساعتی به یک کارمند، کانال پرداخت ممکن است برای چندین ماه یا سال باز نگه داشته شود.
برای باز کردن کانال پرداخت، آلیس قرارداد هوشمند را دیپلوی میکند، اتر را برای تضمین ضمیمه میکند و دریافت کننده مورد نظر و حداکثر مدت زمان وجود کانال را مشخص میکند. در انتهای این بخش این تابع SimplePaymentChannel در قرارداد است.
آلیس با ارسال پیامهای امضا شده به باب، پرداختها را انجام میدهد. این مرحله کاملاً خارج از شبکه اتریوم انجام میشود. پیامها به صورت رمزنگاری شده توسط فرستنده امضا میشوند و سپس مستقیماً به گیرنده ارسال میشوند.
هر پیام شامل اطلاعات زیر است:
در پایان یک سری انتقالها، فقط یک بار کانال پرداخت بسته میشود. به همین دلیل، فقط یکی از پیامهای ارسالی استفاده میشود. به همین دلیل است که هر پیام مجموع مقدار کل اتر بدهکار را به جای مقدار جداگانه کانال پرداخت مشخص میکند. گیرنده به طور طبیعی آخرین پیام را بخاطر اینکه بالاترین جمع کل را دارد، برای بازخرید انتخاب خواهد کرد. دیگر به نانس برای هر پیام نیاز نمیباشد زیرا قرارداد هوشمند فقط به یک پیام پایبند است. برای جلوگیری از استفاده پیامی که در نظر گرفته شده برای یک کانال پرداخت در کانال دیگر، از آدرس قرارداد هوشمند همچنان استفاده میشود.
در اینجا کد جاوا اسکریپت ویرایش شده برای امضای یک پیام به صورت رمزنگاری از بخش قبلی وجود دارد:
function constructPaymentMessage(contractAddress, amount) {
return abi.soliditySHA3(
["address", "uint256"],
[contractAddress, amount]
);
}
function signMessage(message, callback) {
web3.eth.personal.sign(
"0x" + message.toString("hex"),
web3.eth.defaultAccount,
callback
);
}
// contractAddress is used to prevent cross-contract replay attacks.
// amount, in wei, specifies how much Ether should be sent.
function signPayment(contractAddress, amount, callback) {
var message = constructPaymentMessage(contractAddress, amount);
signMessage(message, callback);
}
هنگامی که باب آماده دریافت وجوه خود باشد، وقت آن است که با فراخوانی تابع close در قرارداد هوشمند کانال پرداخت را ببندید. بستن کانال به گیرنده اتری که بدهکار است را پرداخت میکند و قرارداد را از بین میبرد و اتر باقی مانده را برای آلیس میفرستد. برای بستن کانال، باب باید پیامی را امضا کند که توسط آلیس امضا شده باشد. قرارداد هوشمند باید تأیید کند که پیام حاوی یک امضای معتبر از طرف فرستنده است. روند انجام این تأیید همان روندی است که گیرنده از آن استفاده میکند. توابع سالیدیتی isValidSignature و recoverSigner درست همانند رونوشتهای جاوا اسکریپت در بخش قبلی با تابع آخری که از قرارداد ReceiverPays گرفته شده کار میکند.
فقط گیرنده کانال پرداخت میتواند تابع close را فراخوانی کند، که به طور طبیعی جدیدترین پیام پرداخت را ارسال میکند زیرا این پیام بیشترین مجموع بدهی را دارد. اگر فرستنده اجازه فراخوانی این تابع را داشته باشد، میتواند پیامی با مقدار کمتری ارائه دهد و گیرنده را از آنچه طلبکار است، فریب دهد. تابع تأیید میکند که پیام امضا شده با پارامترهای داده شده مطابقت دارد. اگر همه چیز بررسی شود، به گیرنده بخشی از اتر ارسال می شود، و بقیه را از طریق selfdestruct برای فرستنده ارسال میکند. تابع close را میتوانید در قراردادِ کامل مشاهده کنید.
باب میتواند در هر زمان کانال پرداخت را ببندد، اما اگر آنها موفق به انجام این کار نشوند، آلیس به راهی برای بازیابی وجوه پس انداز شده خود نیاز دارد. زمان انقضا در زمان استقرار قرارداد تعیین میشود. پس از رسیدن به این زمان، آلیس میتواند با فراخوانی تابع claimTimeout وجوه خود را پس بگیرد. تابع claimTimeout را میتوانید در قرارداد کامل مشاهده کنید. بعد از فراخوانی این تابع، باب دیگر نمیتواند هیچ اتری دریافت کند، بنابراین مهم است که باب قبل از رسیدن به زمان انقضا، کانال را ببندد.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract SimplePaymentChannel {
address payable public sender; // The account sending payments.
address payable public recipient; // The account receiving the payments.
uint256 public expiration; // Timeout in case the recipient never closes.
constructor(address payable recipientAddress, uint256 duration) payable {
sender = payable(msg.sender);
recipient = recipientAddress;
expiration = block.timestamp + duration;
}
/// The recipient can close the channel at any time by presenting a
/// signed amount from the sender. The recipient will be sent that amount,
/// and the remainder will go back to the sender.
function close(uint256 amount, bytes memory signature) external {
require(msg.sender == recipient);
require(isValidSignature(amount, signature));
recipient.transfer(amount);
selfdestruct(sender);
}
/// The sender can extend the expiration at any time.
function extend(uint256 newExpiration) external {
require(msg.sender == sender);
require(newExpiration > expiration);
expiration = newExpiration;
}
/// If the timeout is reached without the recipient closing the channel,
/// then the Ether is released back to the sender.
function claimTimeout() external {
require(block.timestamp >= expiration);
selfdestruct(sender);
}
function isValidSignature(uint256 amount, bytes memory signature) internal view returns (bool) {
bytes32 message = prefixed(keccak256(abi.encodePacked(this, amount)));
// Check that the signature is from the payment sender
return recoverSigner(message, signature) == sender;
}
/// All functions below this are just taken from the chapter 'creating and verifying signatures'.
function splitSignature(bytes memory sig) internal pure returns (uint8 v, bytes32 r, bytes32 s) {
require(sig.length == 65);
assembly {
// First 32 bytes, after the length prefix
r := mload(add(sig, 32))
// Second 32 bytes
s := mload(add(sig, 64))
// Final byte (first byte of the next 32 bytes)
v := byte(0, mload(add(sig, 96)))
}
return (v, r, s);
}
function recoverSigner(bytes32 message, bytes memory sig) internal pure returns (address) {
(uint8 v, bytes32 r, bytes32 s) = splitSignature(sig);
return ecrecover(message, v, r, s);
}
/// Builds a prefixed hash to mimic the behavior of eth_sign.
function prefixed(bytes32 hash) internal pure returns (bytes32) {
return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash));
}
}
توجه داشته باشید
تابع splitSignature از همه بررسیهای امنیتی استفاده نمیکند. برای پیاده سازی واقعی باید از یک کتابخانه تست شده با دقت بیشتری استفاده کرد، مانند نسخه openzepplin از این کد.
برخلاف بخش قبلی، پیامهای موجود در یک کانال پرداخت بلافاصله استفاده نمیشوند. گیرنده آخرین پیام را پیگیری میکند و هنگامی که زمان بستن کانال پرداخت باشد، آن را استفاده میکند. این به این معنی است که بسیار مهم میباشد که گیرنده تأیید خود را برای هر پیام انجام دهد. در غیر این صورت هیچ تضمینی وجود ندارد که در پایان گیرنده بتواند وجوه را دریافت کند. گیرنده باید هر پیام را با استفاده از روند زیر تأیید کند:
برای نوشتن این تأیید از کتابخانه ethereumjs-util استفاده خواهیم کرد. مرحله آخر را میتوان به روشهای مختلفی انجام داد، و ما از جاوا اسکریپت استفاده میکنیم. کد زیر تابع constructPaymentMessage را از امضای کد جاوا اسکریپت در بالا گرفتیم:
function prefixed(hash) {
return ethereumjs.ABI.soliditySHA3(
["string", "bytes32"],
["\x19Ethereum Signed Message:\n32", hash]
);
}
function recoverSigner(message, signature) {
var split = ethereumjs.Util.fromRpcSig(signature);
var publicKey = ethereumjs.Util.ecrecover(
message,
split.v,
split.r,
split.s
);
var signer = ethereumjs.Util.pubToAddress(publicKey).toString("hex");
return signer;
}
function isValidSignature(contractAddress, amount, signature, expectedSigner) {
var message = prefixed(constructPaymentMessage(contractAddress, amount));
var signer = recoverSigner(message, signature);
return signer.toLowerCase() === ethereumjs.Util.stripHexPrefix(expectedSigner).toLowerCase();
}
یک رویکرد ماژولی برای ساخت قراردادها، در کاهش پیچیدگیها و بهبود خوانایی، که به شناسایی باگها و آسیب پذیریها هنگام توسعه و بررسی کد کمک میکند. اگر رفتار یا هر ماژول را جداگانه تعیین و کنترل کنید، فعل و انفعالاتی را باید فقط در روابط بین مشخصات ماژول در نظر بگیرید و نه هر قسمت متحرک دیگر قرارداد.
در مثال زیر، قرارداد از روش move کتابخانه Balances برای بررسی اینکه بالانسهای ارسال شده بین آدرسها با آنچه شما انتظار دارید مطابقت دارد، استفاده میکند. به این ترتیب، کتابخانه Balances یک جز جداگانه است که موجودی حسابها را به درستی ردیابی میکند را ارائه میدهد. به راحتی میتوان تأیید کرد که کتابخانه Balances هرگز موجودی یا سرریز منفی تولید نمیکند و مجموع کل موجودی در طول مدت قرارداد ثابت نیست.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.0 <0.9.0;
library Balances {
function move(
mapping(address => uint256) storage balances,
address from,
address to,
uint amount
) internal {
require(balances[from] >= amount, "Insufficient balance");
require(balances[to] + amount >= balances[to], "Overflow error");
balances[from] -= amount;
balances[to] += amount;
}
}
contract Token {
mapping(address => uint256) balances;
using Balances for *;
mapping(address => mapping(address => uint256)) allowed;
event Transfer(address from, address to, uint amount);
event Approval(address owner, address spender, uint amount);
function transfer(address to, uint amount) external returns (bool success) {
balances.move(msg.sender, to, amount);
emit Transfer(msg.sender, to, amount);
return true;
}
function transferFrom(address from, address to, uint amount) external returns (bool success) {
require(allowed[from][msg.sender] >= amount, "Allowance exceeded");
allowed[from][msg.sender] -= amount;
balances.move(from, to, amount);
emit Transfer(from, to, amount);
return true;
}
function approve(address spender, uint tokens) external returns (bool success) {
require(allowed[msg.sender][spender] == 0, "Previous allowance not cleared");
allowed[msg.sender][spender] = tokens;
emit Approval(msg.sender, spender, tokens);
return true;
}
function balanceOf(address tokenOwner) external view returns (uint balance) {
return balances[tokenOwner];
}
}