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

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"