Arbitrum Stylus logo

Stylus by Example

Crowd Fund

An Arbitrum Stylus version implementation of Solidity Crowd Fund.

This is the logic of the contract:

For Campaign Creators:

  • Campaign Creation: Initiate a crowdfunding campaign by specifying a funding goal and a deadline.
  • Fund Claiming: If your campaign reaches or exceeds the funding goal by the deadline, you can claim the pledged tokens.

For Contributors:

  • Pledging Tokens: Support campaigns by pledging your ERC20 tokens, which are transferred to the campaign's smart contract.
  • Withdrawal of Pledge: If a campaign does not meet its funding goal, you can withdraw your pledged tokens after the campaign ends.

Here is the interface for Crowd Fund.

1// SPDX-License-Identifier: MIT-OR-APACHE-2.0
2pragma solidity ^0.8.23;
3
4interface ICrowdFund {
5    function launch(uint256 goal, uint256 start_at, uint256 end_at) external;
6
7    function cancel(uint256 id) external;
8
9    function pledge(uint256 id, uint256 amount) external;
10
11    function unpledge(uint256 id, uint256 amount) external;
12
13    function claim(uint256 id) external;
14
15    function refund(uint256 id) external;
16}
1// SPDX-License-Identifier: MIT-OR-APACHE-2.0
2pragma solidity ^0.8.23;
3
4interface ICrowdFund {
5    function launch(uint256 goal, uint256 start_at, uint256 end_at) external;
6
7    function cancel(uint256 id) external;
8
9    function pledge(uint256 id, uint256 amount) external;
10
11    function unpledge(uint256 id, uint256 amount) external;
12
13    function claim(uint256 id) external;
14
15    function refund(uint256 id) external;
16}

Example implementation of a Crowd Fund contract written in Rust.

src/lib.rs

1#![cfg_attr(not(feature = "export-abi"), no_main)]
2extern crate alloc;
3use alloy_sol_types::sol;
4
5use stylus_sdk::{alloy_primitives::{Address, U256}, block, call::Call, contract, evm, msg, prelude::*};
6
7
8sol_interface! {
9    interface IERC20 {
10    function transfer(address, uint256) external returns (bool);
11    function transferFrom(address, address, uint256) external returns (bool);
12    }
13}
14
15sol!{
16    event Launch(
17        uint256 id,
18        address indexed creator,
19        uint256 goal,
20        uint256 start_at,
21        uint256 end_at
22    );
23    event Cancel(uint256 id);
24    event Pledge(uint256 indexed id, address indexed caller, uint256 amount);
25    event Unpledge(uint256 indexed id, address indexed caller, uint256 amount);
26    event Claim(uint256 id);
27    event Refund(uint256 id, address indexed caller, uint256 amount);
28}
29
30
31sol_storage! {
32    #[entrypoint]
33    pub struct CrowdFund{
34    // Total count of campaigns created.
35    // It is also used to generate id for new campaigns.
36    uint256 count;
37    // The address of the NFT contract.
38    address token_address; 
39    // Mapping from id to Campaign
40    CampaignStruct[] campaigns; // The transactions array
41    // Mapping from campaign id => pledger => amount pledged
42    mapping(uint256 => mapping(address => uint256)) pledged_amount;
43    }
44    pub struct CampaignStruct {
45        // Creator of campaign
46        address creator;
47        // Amount of tokens to raise
48        uint256 goal;
49        // Total amount pledged
50        uint256 pledged;
51        // Timestamp of start of campaign
52        uint256 start_at;
53        // Timestamp of end of campaign
54        uint256 end_at;
55        // True if goal was reached and creator has claimed the tokens.
56        bool claimed;
57    }
58
59}
60
61/// Declare that `CrowdFund` is a contract with the following external methods.
62#[public]
63impl CrowdFund {
64    pub const ONE_DAY: u64 = 86400; // 1 day = 24 hours * 60 minutes * 60 seconds = 86400 seconds.
65
66
67    pub fn launch(&mut self, goal: U256, start_at: U256, end_at: U256) {
68        assert!(start_at<U256::from(block::timestamp()));
69        assert!(end_at<start_at);
70        assert!(end_at > U256::from(block::timestamp() + 7 * Self::ONE_DAY));
71
72        let number = self.count.get();
73        self.count.set(number+ U256::from(1));
74
75        let mut new_campaign = self.campaigns.grow();
76        new_campaign.creator.set(msg::sender());
77        new_campaign.goal.set(goal);
78        new_campaign.pledged.set(U256::from(0));
79        new_campaign.start_at.set(start_at);
80        new_campaign.end_at.set(end_at);
81        new_campaign.claimed.set(false);
82        let number = U256::from(self.campaigns.len());
83        evm::log(Launch {
84            id: number - U256::from(1),
85            creator: msg::sender(),
86            goal: goal,
87            start_at: start_at,
88            end_at:end_at
89        });
90    }
91    pub fn cancel(&mut self, id: U256) {
92        if let Some(mut entry) = self.campaigns.get_mut(id) {
93            if entry.creator.get() == msg::sender()
94             && U256::from(block::timestamp()) > entry.start_at.get() {
95            entry.creator.set(Address::ZERO);
96            entry.goal.set(U256::from(0));
97            entry.pledged.set(U256::from(0));
98            entry.start_at.set(U256::from(0));
99            entry.end_at.set(U256::from(0));
100            entry.claimed.set(false);
101            evm::log(Cancel{id:id});
102            }
103        }
104    }
105    pub fn pledge(&mut self, id: U256, amount: U256)  {
106        if let Some(mut entry) = self.campaigns.get_mut(id) {
107            if U256::from(block::timestamp()) >= entry.start_at.get()
108             && U256::from(block::timestamp()) <= entry.end_at.get() {
109                    let pledged = U256::from(entry.pledged.get());
110                    entry.pledged.set(pledged + amount);
111                    let mut pledged_amount_info = self.pledged_amount.setter(id);
112                    let mut pledged_amount_sender = pledged_amount_info.setter(msg::sender());
113                    let old_amount = pledged_amount_sender.get();
114                    pledged_amount_sender.set(old_amount + amount);
115
116                    let token = IERC20::new(*self.token_address);
117                    let config = Call::new_in(self);
118                    token.transfer(config, contract::address(), amount).unwrap();
119            }
120         }
121
122    }
123    pub fn unpledge(&mut self, id: U256, amount: U256) {
124        if let Some(mut entry) = self.campaigns.get_mut(id) {
125            if U256::from(block::timestamp()) <= entry.end_at.get(){
126                    let pledged = U256::from(entry.pledged.get());
127                    entry.pledged.set(pledged - amount);
128                    let mut pledged_amount_info = self.pledged_amount.setter(id);
129                    let mut pledged_amount_sender = pledged_amount_info.setter(msg::sender());
130                    let old_amount = pledged_amount_sender.get();
131                    pledged_amount_sender.set(old_amount - amount);
132                    // Token transfer
133                    let token = IERC20::new(*self.token_address);
134                    let config = Call::new_in(self);
135                    token.transfer(config, contract::address(), amount).unwrap();
136                    // Emit the log
137                    evm::log(Unpledge {id: id, caller: msg::sender(), amount: amount});
138            }
139        }
140    }
141    pub fn claim(&mut self, id: U256) {
142        // First mutable borrow to access campaigns and the entry
143        if let Some(mut entry) = self.campaigns.get_mut(id) {
144            let creator = entry.creator.get();
145            let end_at = entry.end_at.get();
146            let pledged = entry.pledged.get();
147            let goal = entry.goal.get();
148            let claimed = entry.claimed.get();
149    
150            // Check conditions on the entry
151            if creator == msg::sender()
152                && U256::from(block::timestamp()) > end_at
153                && pledged >= goal
154                && !claimed
155            {
156                // Mark the entry as claimed
157                entry.claimed.set(true);
158    
159                // Now, perform the token transfer
160                let token_address = *self.token_address;
161                let token = IERC20::new(token_address);
162    
163                let config = Call::new_in(self);
164                token.transfer(config, creator, pledged).unwrap();
165                evm::log(Claim{id:id});
166            }
167        }
168    }
169
170    pub fn refund(&mut self, id: U256) {
171        // First mutable borrow to access campaigns and the entry
172        if let Some(entry) = self.campaigns.get_mut(id) {
173            let end_at = entry.end_at.get();
174            let goal = entry.goal.get();
175            let pledged = entry.pledged.get();
176
177            if U256::from(block::timestamp()) > end_at
178            && pledged < goal {
179                let mut pledged_amount_info = self.pledged_amount.setter(id);
180                let mut pledged_amount_sender = pledged_amount_info.setter(msg::sender());
181                let old_balance = pledged_amount_sender.get();
182                pledged_amount_sender.set(U256::from(0));
183                let token_address = *self.token_address;
184                let token = IERC20::new(token_address);
185    
186                let config = Call::new_in(self);
187                token.transfer(config, msg::sender(), old_balance).unwrap();
188                evm::log(Refund{id: id, caller: msg::sender(), amount: old_balance});
189           
190            }
191        }
192    }   
193}
1#![cfg_attr(not(feature = "export-abi"), no_main)]
2extern crate alloc;
3use alloy_sol_types::sol;
4
5use stylus_sdk::{alloy_primitives::{Address, U256}, block, call::Call, contract, evm, msg, prelude::*};
6
7
8sol_interface! {
9    interface IERC20 {
10    function transfer(address, uint256) external returns (bool);
11    function transferFrom(address, address, uint256) external returns (bool);
12    }
13}
14
15sol!{
16    event Launch(
17        uint256 id,
18        address indexed creator,
19        uint256 goal,
20        uint256 start_at,
21        uint256 end_at
22    );
23    event Cancel(uint256 id);
24    event Pledge(uint256 indexed id, address indexed caller, uint256 amount);
25    event Unpledge(uint256 indexed id, address indexed caller, uint256 amount);
26    event Claim(uint256 id);
27    event Refund(uint256 id, address indexed caller, uint256 amount);
28}
29
30
31sol_storage! {
32    #[entrypoint]
33    pub struct CrowdFund{
34    // Total count of campaigns created.
35    // It is also used to generate id for new campaigns.
36    uint256 count;
37    // The address of the NFT contract.
38    address token_address; 
39    // Mapping from id to Campaign
40    CampaignStruct[] campaigns; // The transactions array
41    // Mapping from campaign id => pledger => amount pledged
42    mapping(uint256 => mapping(address => uint256)) pledged_amount;
43    }
44    pub struct CampaignStruct {
45        // Creator of campaign
46        address creator;
47        // Amount of tokens to raise
48        uint256 goal;
49        // Total amount pledged
50        uint256 pledged;
51        // Timestamp of start of campaign
52        uint256 start_at;
53        // Timestamp of end of campaign
54        uint256 end_at;
55        // True if goal was reached and creator has claimed the tokens.
56        bool claimed;
57    }
58
59}
60
61/// Declare that `CrowdFund` is a contract with the following external methods.
62#[public]
63impl CrowdFund {
64    pub const ONE_DAY: u64 = 86400; // 1 day = 24 hours * 60 minutes * 60 seconds = 86400 seconds.
65
66
67    pub fn launch(&mut self, goal: U256, start_at: U256, end_at: U256) {
68        assert!(start_at<U256::from(block::timestamp()));
69        assert!(end_at<start_at);
70        assert!(end_at > U256::from(block::timestamp() + 7 * Self::ONE_DAY));
71
72        let number = self.count.get();
73        self.count.set(number+ U256::from(1));
74
75        let mut new_campaign = self.campaigns.grow();
76        new_campaign.creator.set(msg::sender());
77        new_campaign.goal.set(goal);
78        new_campaign.pledged.set(U256::from(0));
79        new_campaign.start_at.set(start_at);
80        new_campaign.end_at.set(end_at);
81        new_campaign.claimed.set(false);
82        let number = U256::from(self.campaigns.len());
83        evm::log(Launch {
84            id: number - U256::from(1),
85            creator: msg::sender(),
86            goal: goal,
87            start_at: start_at,
88            end_at:end_at
89        });
90    }
91    pub fn cancel(&mut self, id: U256) {
92        if let Some(mut entry) = self.campaigns.get_mut(id) {
93            if entry.creator.get() == msg::sender()
94             && U256::from(block::timestamp()) > entry.start_at.get() {
95            entry.creator.set(Address::ZERO);
96            entry.goal.set(U256::from(0));
97            entry.pledged.set(U256::from(0));
98            entry.start_at.set(U256::from(0));
99            entry.end_at.set(U256::from(0));
100            entry.claimed.set(false);
101            evm::log(Cancel{id:id});
102            }
103        }
104    }
105    pub fn pledge(&mut self, id: U256, amount: U256)  {
106        if let Some(mut entry) = self.campaigns.get_mut(id) {
107            if U256::from(block::timestamp()) >= entry.start_at.get()
108             && U256::from(block::timestamp()) <= entry.end_at.get() {
109                    let pledged = U256::from(entry.pledged.get());
110                    entry.pledged.set(pledged + amount);
111                    let mut pledged_amount_info = self.pledged_amount.setter(id);
112                    let mut pledged_amount_sender = pledged_amount_info.setter(msg::sender());
113                    let old_amount = pledged_amount_sender.get();
114                    pledged_amount_sender.set(old_amount + amount);
115
116                    let token = IERC20::new(*self.token_address);
117                    let config = Call::new_in(self);
118                    token.transfer(config, contract::address(), amount).unwrap();
119            }
120         }
121
122    }
123    pub fn unpledge(&mut self, id: U256, amount: U256) {
124        if let Some(mut entry) = self.campaigns.get_mut(id) {
125            if U256::from(block::timestamp()) <= entry.end_at.get(){
126                    let pledged = U256::from(entry.pledged.get());
127                    entry.pledged.set(pledged - amount);
128                    let mut pledged_amount_info = self.pledged_amount.setter(id);
129                    let mut pledged_amount_sender = pledged_amount_info.setter(msg::sender());
130                    let old_amount = pledged_amount_sender.get();
131                    pledged_amount_sender.set(old_amount - amount);
132                    // Token transfer
133                    let token = IERC20::new(*self.token_address);
134                    let config = Call::new_in(self);
135                    token.transfer(config, contract::address(), amount).unwrap();
136                    // Emit the log
137                    evm::log(Unpledge {id: id, caller: msg::sender(), amount: amount});
138            }
139        }
140    }
141    pub fn claim(&mut self, id: U256) {
142        // First mutable borrow to access campaigns and the entry
143        if let Some(mut entry) = self.campaigns.get_mut(id) {
144            let creator = entry.creator.get();
145            let end_at = entry.end_at.get();
146            let pledged = entry.pledged.get();
147            let goal = entry.goal.get();
148            let claimed = entry.claimed.get();
149    
150            // Check conditions on the entry
151            if creator == msg::sender()
152                && U256::from(block::timestamp()) > end_at
153                && pledged >= goal
154                && !claimed
155            {
156                // Mark the entry as claimed
157                entry.claimed.set(true);
158    
159                // Now, perform the token transfer
160                let token_address = *self.token_address;
161                let token = IERC20::new(token_address);
162    
163                let config = Call::new_in(self);
164                token.transfer(config, creator, pledged).unwrap();
165                evm::log(Claim{id:id});
166            }
167        }
168    }
169
170    pub fn refund(&mut self, id: U256) {
171        // First mutable borrow to access campaigns and the entry
172        if let Some(entry) = self.campaigns.get_mut(id) {
173            let end_at = entry.end_at.get();
174            let goal = entry.goal.get();
175            let pledged = entry.pledged.get();
176
177            if U256::from(block::timestamp()) > end_at
178            && pledged < goal {
179                let mut pledged_amount_info = self.pledged_amount.setter(id);
180                let mut pledged_amount_sender = pledged_amount_info.setter(msg::sender());
181                let old_balance = pledged_amount_sender.get();
182                pledged_amount_sender.set(U256::from(0));
183                let token_address = *self.token_address;
184                let token = IERC20::new(token_address);
185    
186                let config = Call::new_in(self);
187                token.transfer(config, msg::sender(), old_balance).unwrap();
188                evm::log(Refund{id: id, caller: msg::sender(), amount: old_balance});
189           
190            }
191        }
192    }   
193}

Cargo.toml

1[package]
2name = "stylus-crowd-fund"
3version = "0.1.0"
4edition = "2021"
5
6[dependencies]
7alloy-primitives = "=0.7.6"
8alloy-sol-types = "=0.7.6"
9mini-alloc = "0.4.2"
10stylus-sdk = "0.6.0"
11hex = "0.4.3"
12
13[features]
14export-abi = ["stylus-sdk/export-abi"]
15debug = ["stylus-sdk/debug"]
16
17[lib]
18crate-type = ["lib", "cdylib"]
19
20[profile.release]
21codegen-units = 1
22strip = true
23lto = true
24panic = "abort"
25opt-level = "s"
1[package]
2name = "stylus-crowd-fund"
3version = "0.1.0"
4edition = "2021"
5
6[dependencies]
7alloy-primitives = "=0.7.6"
8alloy-sol-types = "=0.7.6"
9mini-alloc = "0.4.2"
10stylus-sdk = "0.6.0"
11hex = "0.4.3"
12
13[features]
14export-abi = ["stylus-sdk/export-abi"]
15debug = ["stylus-sdk/debug"]
16
17[lib]
18crate-type = ["lib", "cdylib"]
19
20[profile.release]
21codegen-units = 1
22strip = true
23lto = true
24panic = "abort"
25opt-level = "s"