Kitle fonlaması, yaratıcıların destekçilerle doğrudan bağlantı kurmasına ve geleneksel finansal kurumları devre dışı bırakmasına olanak tanıyarak projelerin, toplumsal amaçların ve işletmelerin fon toplama biçimini değiştirdi. Blockchain teknolojisi, şeffaflık, merkeziyetsizlik ve değiştirilemezlik ekleyerek bu modeli geliştiriyor; hem yaratıcılar hem de katkıda bulunanlar için daha adil bir sistem sağlıyor.
Bu makale, Linea gibi EVM uyumlu blockchainler için tasarlanmış Solidity tabanlı bir Kitle Fonlaması Akıllı Sözleşmesi oluşturma sürecinde size rehberlik edecek. Sözleşme, kullanıcıların kampanya oluşturmasına, güvenli bir şekilde fon katkısında bulunmasına, hedefe ulaşıldığında fon çekmesine ve kampanya başarısız olursa geri ödeme talep etmesine olanak tanıyacak. Akıllı sözleşme otomasyonundan yararlanarak platform, aracıları ortadan kaldırır ve hesap verebilirliği zorunlu kılar.
Sözleşmenin uygulaması, özellikleri ve işlevselliği incelenerek geliştiriciler, merkeziyetsiz kitle fonlaması platformları oluşturma konusunda değerli bilgiler edinecek. İster bir blockchain meraklısı, ister geliştirici, ister girişimci olun; bu rehber, akıllı sözleşmelerin kitle fonlamasını daha güvenli, verimli ve güven gerektirmez hale getirerek nasıl devrim yaratabileceğini gösterecek.
Ön Koşullar
Solidity hakkında temel bir anlayışa sahip olmalısınız.
Bilgisayarınızda Nodejs ve Foundry kurulu olmalıdır.
Sözleşmeye Genel Bakış
Bu sözleşme şunları mümkün kılar:
Kampanya oluşturma: Kullanıcılar bir hedef ve süre belirleyerek bağış kampanyaları başlatabilir.
Katkılar: Kullanıcılar aktif kampanyalara fon katkısında bulunabilir.
Fon çekme: Kampanya oluşturucuları, kampanya son tarihinden sonra bağış hedefi karşılanırsa fonları çekebilir.
Geri ödemeler: Katkıda bulunanlar, hedef son tarih içinde karşılanmazsa katkılarını geri alabilir.
Proje Kurulumu
Bir Foundry projesi başlatmak için aşağıdaki komutu çalıştırın. Akıllı sözleşmeyi oluşturmak için Foundry çerçevesini kullanacağız.
forge init crowdfunding
Crowdfunding klasörünü VS Code'da veya favori kod düzenleyicinizde açın ve scripts/counter.s.sol, src/counter.sol ile test/counter.t.sol dosyalarını silin.
Tüm bağımlılıkları yükleyin
forge install foundry-rs/forge-std --no-commit
Kitle fonlaması akıllı sözleşmesinin tam kodu
src klasöründe bir crowdfunding.sol dosyası oluşturun ve aşağıdaki kodu dosyaya ekleyin.
pragma solidity 0.8.18;
contract Crowdfunding {
struct Campaign {
address creator;
uint256 goal;
uint256 deadline;
uint256 amountRaised;
bool isWithdrawn;
mapping(address => uint256) contributions;
}
uint256 public campaignCount = 0;
mapping(uint256 => Campaign) public campaigns;
event CampaignCreated(uint256 campaignId, address creator, uint256 goal, uint256 deadline);
event ContributionMade(uint256 campaignId, address contributor, uint256 amount);
event FundsWithdrawn(uint256 campaignId, uint256 amount);
event RefundIssued(uint256 campaignId, address contributor, uint256 amount);
modifier campaignExists(uint256 campaignId) {
require(campaignId < campaignCount, "Campaign does not exist");
_;
}
modifier onlyCreator(uint256 campaignId) {
require(msg.sender == campaigns[campaignId].creator, "Only the campaign creator can perform this action");
_;
}
modifier beforeDeadline(uint256 campaignId) {
require(block.timestamp <= campaigns[campaignId].deadline, "Campaign deadline has passed");
_;
}
modifier afterDeadline(uint256 campaignId) {
require(block.timestamp > campaigns[campaignId].deadline, "Campaign deadline has not passed yet");
_;
}
function createCampaign(uint256 goal, uint256 duration) external {
require(goal > 0, "Goal must be greater than zero");
require(duration > 0, "Duration must be greater than zero");
Campaign storage newCampaign = campaigns[campaignCount];
newCampaign.creator = msg.sender;
newCampaign.goal = goal;
newCampaign.deadline = block.timestamp + duration;
emit CampaignCreated(campaignCount, msg.sender, goal, newCampaign.deadline);
campaignCount++;
}
function contribute(uint256 campaignId) external payable campaignExists(campaignId) beforeDeadline(campaignId) {
require(msg.value > 0, "Contribution must be greater than zero");
Campaign storage campaign = campaigns[campaignId];
campaign.amountRaised += msg.value;
campaign.contributions[msg.sender] += msg.value;
emit ContributionMade(campaignId, msg.sender, msg.value);
}
function withdrawFunds(uint256 campaignId) external campaignExists(campaignId) onlyCreator(campaignId) afterDeadline(campaignId) {
Campaign storage campaign = campaigns[campaignId];
require(campaign.amountRaised >= campaign.goal, "Funding goal not reached");
require(!campaign.isWithdrawn, "Funds already withdrawn");
campaign.isWithdrawn = true;
(bool sent, ) = campaign.creator.call{value: campaign.amountRaised}("");
require(sent, "Failed to send Ether");
emit FundsWithdrawn(campaignId, campaign.amountRaised);
}
function requestRefund(uint256 campaignId) external campaignExists(campaignId) afterDeadline(campaignId) {
Campaign storage campaign = campaigns[campaignId];
require(campaign.amountRaised < campaign.goal, "Campaign succeeded, refunds not allowed");
uint256 contribution = campaign.contributions[msg.sender];
require(contribution > 0, "No contributions found for this campaign");
campaign.contributions[msg.sender] = 0;
(bool sent, ) = msg.sender.call{value: contribution}("");
require(sent, "Failed to send Ether");
emit RefundIssued(campaignId, msg.sender, contribution);
}
}
Kodun temel özellikleri
Kampanya yapısı Sözleşme, kampanya ayrıntılarını depolamak için Campaign adlı bir struct kullanır.
struct Campaign {
address creator;
uint256 goal;
uint256 deadline;
uint256 amountRaised;
bool isWithdrawn;
mapping(address \=\> uint256) contributions;
}
Kampanya sayısı Her kampanya, artan bir campaignId ile benzersiz şekilde tanımlanır; bu da ölçeklenebilirliği sağlar ve birden fazla kampanyanın yönetimini kolaylaştırır.
uint256 public campaignCount = 0;
mapping(uint256 => Campaign) public campaigns;
Kampanya oluşturma
createCampaign fonksiyonu, bir kullanıcının kampanya başlatmasına olanak tanır. İki parametre alır: goal ve duration. goal, kullanıcının toplamak istediği hedef miktarı temsil ederken duration, bu hedefe ulaşmak için ayrılan süreyi saniye cinsinden belirtir.
Fonksiyon, geçersiz girişleri önlemek için hem goal hem de duration değerlerinin sıfırdan büyük olmasını sağlar. Kampanya ayrıntıları daha sonra Campaign struct'ında depolanır ve campaignId buna göre atanır.
Yeni oluşturulan kampanyaların benzersiz campaignId'lere sahip olmasını ve önceki kampanyalarla çakışmamasını sağlamak için campaignCount artırılır.
function createCampaign(uint256 goal, uint256 duration) external {
require(goal > 0, "Goal must be greater than zero");
require(duration > 0, "Duration must be greater than zero");
Campaign storage newCampaign = campaigns[campaignCount];
newCampaign.creator = msg.sender;
newCampaign.goal = goal;
newCampaign.deadline = block.timestamp + duration;
emit CampaignCreated(campaignCount, msg.sender, goal, newCampaign.deadline);
campaignCount++;
}
Katkı mekanizması
contribute fonksiyonu, kullanıcıların aktif kampanyaları fonlamasına olanak tanır. Her kampanya için benzersiz tanımlayıcı olarak işlev gören ve katkının nereye tahsis edileceğini belirleyen campaignId adlı bir parametre alır.
Fonksiyon, kullanıcıların sıfır miktarda katkıda bulunmamasını sağlar. Kampanya için amountRaised değerini takip eder ve her kullanıcı tarafından yapılan toplam katkıyı kaydeder.
function contribute(uint256 campaignId) external payable campaignExists(campaignId) beforeDeadline(campaignId) {
require(msg.value > 0, "Contribution must be greater than zero");
Campaign storage campaign = campaigns[campaignId];
campaign.amountRaised += msg.value;
campaign.contributions[msg.sender] += msg.value;
emit ContributionMade(campaignId, msg.sender, msg.value);
}
contribute fonksiyonundaki campaignExists modifier'ı, campaignId'nin geçerli olup olmadığını doğrular ve kampanya mevcut değilse hata fırlatır.
modifier campaignExists(uint256 campaignId) {
require(campaignId < campaignCount, "Campaign does not exist");
_;
}
beforeDeadline modifier'ı, kampanyanın son tarihinin geçip geçmediğini doğrular ve geçmişse hata fırlatır.
modifier beforeDeadline(uint256 campaignId) {
require(block.timestamp <= campaigns[campaignId].deadline, "Campaign deadline has passed");
_;
}
Fon çekme
Kampanya oluşturucuları, her kampanya için benzersiz tanımlayıcı olarak işlev gören campaignId adlı bir parametre alan withdrawFunds fonksiyonunu kullanarak fon çekebilir.
Yalnızca kampanya oluşturucu fon çekebilir. Başka bir kullanıcı bunu denerse fonksiyon hata fırlatır.
Fonksiyon, bağış hedefinin karşılandığını ve fonların daha önce çekilmediğini doğrular. Hedef karşılanmamışsa veya fonlar zaten çekilmişse fonksiyon hata fırlatır.
Başarılı bir çekme işleminin ardından fonksiyon, isWithdrawn durumunu true olarak günceller ve fonları kampanya oluşturucusuna aktarır.
``Solidity function withdrawFunds(uint256 campaignId) external campaignExists(campaignId) onlyCreator(campaignId) afterDeadline(campaignId) { Campaign storage campaign = campaigns[campaignId]; require(campaign.amountRaised >= campaign.goal, "Funding goal not reached"); require(!campaign.isWithdrawn, "Funds already withdrawn");
campaign.isWithdrawn = true;
(bool sent, ) = campaign.creator.call{value: campaign.amountRaised}("");
require(sent, "Failed to send Ether");
emit FundsWithdrawn(campaignId, campaign.amountRaised);
}
The `onlyCreator` modifier verifies whether the caller is the campaign creator and throws an error if they are not.
``Solidity
modifier onlyCreator(uint256 campaignId) {
require(msg.sender == campaigns[campaignId].creator, "Only the campaign creator can perform this action");
_;
}
afterDeadline modifier'ı, kampanyanın son tarihinin geçip geçmediğini doğrular ve geçmemişse hata fırlatır.
modifier afterDeadline(uint256 campaignId) {
require(block.timestamp > campaigns[campaignId].deadline, "Campaign deadline has not passed yet");
_;
}
Başarısız kampanyalar için geri ödemeler
Katkıda bulunanlar, kampanya başarısız olursa katkılarını geri çekebilir. Fonksiyon, her kampanyayı benzersiz şekilde tanımlayan campaignId adlı bir parametre alır.
Kampanyanın bağış hedefini karşılamadığını ve kullanıcının katkıda bulunduğunu doğrular. Her iki koşul da karşılanırsa fonksiyon, kullanıcının katkısını sıfıra sıfırlar ve geri ödemeyi işler.
function requestRefund(uint256 campaignId) external campaignExists(campaignId) afterDeadline(campaignId) {
Campaign storage campaign = campaigns[campaignId];
require(campaign.amountRaised < campaign.goal, "Campaign succeeded, refunds not allowed");
uint256 contribution = campaign.contributions[msg.sender];
require(contribution > 0, "No contributions found for this campaign");
campaign.contributions[msg.sender] = 0;
(bool sent, ) = msg.sender.call{value: contribution}("");
require(sent, "Failed to send Ether");
emit RefundIssued(campaignId, msg.sender, contribution);
}
Olaylar
Sözleşme, temel eylemler için olaylar yayar:
CampaignCreated: Yeni bir kampanyanın ayrıntılarını kaydeder.
ContributionMade: Bir kampanyaya yapılan katkıları kaydeder.
FundsWithdrawn: Başarılı fon çekme işlemlerini kaydeder.
RefundIssued: Katkıda bulunanlara verilen geri ödemeleri kaydeder.
event CampaignCreated(uint256 campaignId, address creator, uint256 goal, uint256 deadline);
event ContributionMade(uint256 campaignId, address contributor, uint256 amount);
event FundsWithdrawn(uint256 campaignId, uint256 amount);
event RefundIssued(uint256 campaignId, address contributor, uint256 amount);
Testler
Test klasöründe bir crowdfunding.t.sol dosyası oluşturun ve aşağıdaki kodu dosyaya ekleyin.
pragma solidity ^0.8.13;
import {Test, console} from "forge-std/Test.sol";
import {Crowdfunding} from "../src/Crowdfunding.sol";
contract CounterTest is Test {
Crowdfunding public crowd;
address public user1 = makeAddr("user1");
address public user2 = makeAddr("user2");
address public user3 = makeAddr("user3");
function setUp() public {
crowd = new Crowdfunding();
}
function test_createCampaign() public {
vm.startPrank(user1);
crowd.createCampaign(10 ether, 10 hours);
}
function test_contributeCampaign() public {
vm.startPrank(user1);
crowd.createCampaign(10 ether, 10 hours);
deal(user2, 10 ether);
deal(user3, 10 ether);
vm.startPrank(user2);
crowd.contribute{value: 6 ether}(0);
vm.startPrank(user3);
crowd.contribute{value: 6 ether}(0);
}
function test_withdrawCampaign() public {
vm.startPrank(user1);
crowd.createCampaign(10 ether, 10 hours);
deal(user2, 10 ether);
deal(user3, 10 ether);
vm.startPrank(user2);
crowd.contribute{value: 6 ether}(0);
vm.startPrank(user3);
crowd.contribute{value: 6 ether}(0);
skip(11 hours);
console.log("b4", user1.balance);
vm.startPrank(user1);
crowd.withdrawFunds(0);
console.log("after",user1.balance);
}
function test_requestCampaign() public {
vm.startPrank(user1);
crowd.createCampaign(10 ether, 10 hours);
deal(user2, 10 ether);
deal(user3, 10 ether);
vm.startPrank(user2);
crowd.contribute{value: 2 ether}(0);
vm.startPrank(user3);
crowd.contribute{value: 2 ether}(0);
skip(11 hours);
vm.startPrank(user2);
crowd.requestRefund(0);
vm.startPrank(user3);
crowd.requestRefund(0);
console.log("after",user2.balance);
console.log("after",user3.balance);
}
}
Test paketi, Crowdfunding sözleşmesinin temel özellikleri genelinde doğru şekilde çalışmasını sağlar. Aşağıda testlerin dökümü yer almaktadır:
test_createCampaign
Bu test, bir kullanıcının başarıyla bir kitle fonlaması kampanyası oluşturabileceğini doğrular. Kampanya hatasız oluşturulur ve fonksiyonun beklendiği gibi çalıştığı onaylanır.
test_contributeCampaign
Bu test, birden fazla kullanıcının belirli bir kampanyaya katkıda bulunabileceğini doğrular. Testte user1 bir kampanya oluştururken user2 ve user3, Foundry'nin deal fonksiyonunu kullanarak her biri 10 ether alır. Hem user2 hem de user3 kampanyaya 6 ether katkıda bulunur ve sözleşme, kampanyanın toplanan toplam miktarını doğru şekilde günceller.
test_withdrawCampaign
Bu test, kampanya oluşturucusunun kampanya bağış hedefini karşıladıktan ve son tarih geçtikten sonra fon çekebileceğini doğrular. user2 ve user3 kampanyaya katkıda bulunduktan ve 11 saat geçtikten sonra (skip kullanılarak simüle edilir), user1 toplanan fonları çekmek için withdrawFunds'ı çağırır. Fonlar başarıyla aktarılır ve sözleşmenin bakiyesi çekme işlemini yansıtır.
test_requestCampaign
Bu test, kampanyanın bağış hedefini karşılayamaması durumunda katkıda bulunanların geri ödeme talep edebileceğini doğrular. user2 ve user3 kampanyaya katkıda bulunduktan ve 11 saat geçtikten sonra (skip kullanılarak simüle edilir), hem user2 hem de user3 requestRefund'ı çağırır. Her kullanıcı, katkıda bulunduğu ether'i başarıyla geri alır.
Testi çalıştırın
forge test
Linea'da Dağıtım ve Doğrulama
Gerekli ortam değişkenlerini .env dosyasına ekleyin; dosyanın projenin kök dizininde yer aldığından emin olun. Bu değişkenler, blockchain etkileşimleri için gereken hassas kimlik bilgilerini depolar.
LINEA_RPC_URL=https://linea-mainnet.infura.io/v3/
PRIVATE_KEY=*************************************************
Aşağıdaki ayrıntıları foundry.toml dosyasına ekleyin.
[etherscan]
linea = { key = "${LINEA_API_KEY}", url = "https://api-sepolia.lineascan.build/api" }
Bir Makefile oluşturun ve aşağıdaki ayrıntıları ekleyin.
deploy:
forge create src/Crowdfunding.sol:Crowdfunding --rpc-url $(LINEA_RPC_URL) --private-key $(PRIVATE_KEY)
verify:; forge verify-contract --rpc-url $(LINEA_RPC_URL) --chain linea <contract address> src/Crowdfunding.sol:Crowdfunding
Dağıtım
make deploy
Doğrulama
make verify
Sonuç
Bu rehber, Linea blockchain üzerinde merkeziyetsiz bir kitle fonlaması akıllı sözleşmesi oluşturma ve dağıtmanın temellerini ele aldı. Kampanya oluşturma, katkılar, para çekme ve geri ödemeler gibi temel sözleşme işlevlerini inceledik; sözleşmenin davranışını doğrulamak için testler yazdık ve çalıştırdık. Son olarak, sözleşmeyi Linea'da yayına almak için dağıtım ve doğrulama sürecini adım adım anlattık.
Blockchain geliştirmeye yönelik daha fazla uygulamalı eğitim ve derinlemesine içerik için MetaMask Developer YouTube kanalına göz atın ve Twitter üzerinden bizi takip ederek en güncel bilgilerden haberdar olun.