Make Crowdfunding Smart Contract using Solidity and Golang Language

mobin shaterian
Block Magnates
Published in
12 min readDec 27, 2023

--

We want to make smart contracts to solve a real problem in the real world.
Imagine a group of people who want to contribute their money to a specific project. And when we gather the money the owner wants to spend this money on a special vendor. Also, it is necessary to vote and select a vendor by the contributor. We want to guarantee that everything executes without any cheat. The best solution is using smart contracts to keep every thing immutable.

It also can be used in many diffrent topics, such as gathering tax, managment of building, … .

Spending request attempts to withdraw money from the contract and send it to an external address.

Struct Document in solidity

Removal of Unused or Unsafe Features

Mappings outside Storage

  • If a struct or array contains a mapping, it can only be used in storage. Previously, mapping members were silently skipped in memory, which is confusing and error-prone.
  • Assignments to structs or arrays in storage does not work if they contain mappings. Previously, mappings were silently skipped during the copy operation, which is misleading and error-prone.

Storage vs Memory in Solidity

Storage and Memory keywords in Solidity are analogous to Computer’s hard drive and Computer’s RAM. Much like RAM, Memory in Solidity is a temporary place to store data whereas Storage holds data between function calls. The Solidity Smart Contract can use any amount of memory during the execution but once the execution stops, the Memory is completely wiped off for the next execution. Whereas Storage on the other hand is persistent, each execution of the Smart contract has access to the data previously stored on the storage area.

Every transaction on Ethereum Virtual Machine costs us some amount of Gas. The lower the Gas consumption the better is your Solidity code. The Gas consumption of Memory is not very significant as compared to the gas consumption of Storage. Therefore, it is always better to use Memory for intermediate calculations and store the final result in Storage.

  1. State variables and Local Variables of structs, array are always stored in storage by default.
  2. Function arguments are in memory.
  3. Whenever a new instance of an array is created using the keyword ‘memory’, a new copy of that variable is created. Changing the array value of the new instance does not affect the original array.

Mapping key didn’t store in In smart contract we only have a value in smart contract.

Race condition in Smart contract

All transactions in Ethereum are run serially. Just one after another. Everything your transaction does, including calling from one contract to another, happens within the context of your transaction and nothing else runs until your contract is done.

So race conditions are totally not a concern. You can call balanceOf() on another contract, put the result in a local variable, and use it with no worries that the balance in the other contract will change before you're done.

There are a few different ways to prevent race conditions in smart contracts. One common approach is to use locks, as described above. Another approach is to use a concept called timestamping. Timestamping involves assigning a timestamp to each transaction. Transactions are then processed in order of their timestamps, which helps to prevent conflicts.

In general, it is important to be aware of the potential for race conditions when writing smart contracts. There are a number of techniques that can be used to prevent race conditions, but it is important to choose the appropriate technique for the specific application.

Please pay attention to using iteration in smart contracts, It needs more gas to execute.

When we release a new deployment of smart contracts they have a different hash code and they are not related to each other.

Every web3 project follows these steps:

  1. Write solidity and check it with https://remix.ethereum.org
  2. Compile solidity project into JSON and ABI
  3. Deploy compile file on a network
  4. Write tests for check functionality

Lets make it

We have a manager and list of contributors, and minimum value of contribute.

// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity >=0.5.0 <0.9.0;
// linter warnings (red underline) about pragma version can igonored!

// contract code will go here

contract Campaing {

address public manager;
uint256 public minimumContribution;
mapping (address => bool ) public approvers;

constructor(uint256 min ) {
manager = msg.sender;
minimumContribution = min;
}

}

Add contribute with minimum amount in contribute list

    function contibute() public payable {
require(
msg.value >= minimumContribution,
"minimum contribute is required."
);

approvers[msg.sender] = true;
approversCount ++;

}

If the value is less than min we get an error and reduce the gas.

We can check whether the address is contributed or not:

    function isPaied() public view returns (bool) {
return (approvers[msg.sender]);
}

Get balance of smart contract account

    function getBalance() public view returns(uint256){
return (address(this).balance);
}

Now, added a list of requests for each provider. Providers are addressed that the manager wants to spend money on contributors.

    struct Request {
string description;
uint256 value;
address payable recipient;
bool complete;
uint256 approvalCount;
mapping(address => bool) approvals;
}

the manager only can call these function so we need make modifier for them.

 modifier onlyManager() {
require(
msg.sender == manager,
"Only the campaign manager can call this function."
);
_;
}

add request is a function that the manager can make it and create a new provider for spending the contributors

    function addRequest(
string memory description,
uint256 value,
address payable recipient
) public onlyManager {

Request storage req = requests[numRequests++];
req.description = description;
req.value = value;
req.recipient = recipient;
req.complete = false;
req.approvalCount = 0;
}
    function approveRequest(uint256 index) public{

Request storage req = requests[index];
require(
approvers[msg.sender] ,
"Only contributors can approve a specific payment request"
);

require(
!req.approvals[msg.sender],
"You have already voted to approve this request"
);

require(index < numRequests,
"You can vote only in valid index. more than valid value");

require(index >= 0,
"You can vote only in valid index. minus value");

req.approvals[msg.sender] = true;
req.approvalCount++;
}
"water",1000,0xdD870fA1b7C4700F2BD7f44238821C26f7392148

Test of this part needs three different accounts : manager, contributor, provider

  1. manager : deploy contract
  2. contributor: contribute inside the system
  3. manager: create a request for provider
  4. contributor: approve the request

Manager can finalize Request and send money to provider

    function finalizeRequest(uint256 index) public onlyManager {
require(index < numRequests,
"You can vote only in valid index. more than valid");

require(index >= 0,
"You can vote only in valid index. minus value");

Request storage req = requests[index];

require(!req.complete ,
"This request has been completed.");

req.recipient.transfer(req.value);
req.complete = true;
}

Also we need to check the number of votes that send to the request:


require(
req.approvalCount > (approversCount / 2),
"This request needs more approvals before it can be finalized"
);

Final code is exist in my repository:

Deploy Solidity code with Go Ethereum:

Connect to network

type Client struct {
Client *ethclient.Client
}

func (c *Client) NewClient(address string) {

client, err := ethclient.Dial(address)
if err != nil {
log.Fatal(err)
}
fmt.Println("connect to network ...")
c.Client = client
}

Using Ganache

ganache-cli

Define Accounts

Put three account in env file

ADDRESS = http://localhost:8545
MANAGER_ADDRESS = 0xBad21545CDAc5eA56050a5441E435B2437fE4770
MANAGER_PRIVATE_ADDRESS = 0xbcab03c0fb40ccdd5d75b8024731dd772113b02d01132d81c584027d8a7280b9
PROVIDER_ADDRESS = 0xEc89A6a668206286a875BB906B79BbB756f09E22
PROVIDER_PRIVATE_ADDRESS = 0xbf6e3ba07b0ea7a98e5cbd0f0206bcdb35fd9520571c28cc16c803cae702dc89
CONTRIBUTOR_ADDRESS = 0x6BC477cE822a30331D0D5Ce4B5CeA23b2f2eEaEb
CONTRIBUTOR_PRIVATE_ADDRESS = 0x2b7b6e43aca333d79375eb8cb3d225b795480596d4fff6e0abf70c111628a410

Get balance of accounts

type Account struct {
PublicAddress common.Address
PrivateAddress string
Name string
client *ethclient.Client
}

// NewAccount creates a new instance of Account
func NewAccount(publicAddress string,
privateAddress string,
client *client.Client) *Account {
return &Account{
PublicAddress: common.HexToAddress(publicAddress),
client: client.Client,
}
}

func (a *Account) GetBalance() *big.Int {

balance, err := a.client.BalanceAt(context.Background(), a.PublicAddress, nil)
if err != nil {
log.Fatal(err)
}

return balance

}

Result:

go run *.go

connect to network ...
balance manager : 1000000000000000000000
balance provider : 1000000000000000000000
contributor manager : 1000000000000000000000

Install Abigen:

$ cd $GOPATH/src/github.com/ethereum/go-ethereum
$ go build ./cmd/abigen

build .sol

cd contracts/
solc --abi Campaign.sol -o build
solc --bin Campaign.sol -o Campaign.bin

Then abigen can be run again, this time passing Campaign.bin:


abigen --abi ./build/Campaign.abi --pkg main --type Campaign --out Campaign.go --bin ./Campaign.bin/Campaign.bin

If the code does not deploy on network:

or got below error :

vm exception while processing transaction: invalid opcode error
invalid opcode error in ganache 2.7.1
pip3 install solc-select
solc-select use <version> --always-install
solc --version
solc, the solidity compiler commandline interface
Version: 0.8.7+commit.e28d00a7.Linux.g++

Deploy on network with manger account:

First we need get private key from the account

 privateKey, err := crypto.HexToECDSA(strings.TrimPrefix(privateAddress, "0x"))
if err != nil {
log.Fatal(err)
}

// HexToECDSA parses a secp256k1 private key.

Deploy function

type Campaign struct {
instance *contracts.Campaign
txAddress common.Address
tx *types.Transaction
Min *big.Int
client *ethclient.Client
}

func NewCampaign(min *big.Int, client *ethclient.Client) *Campaign {
return &Campaign{
Min: min,
client: client,
}
}

func (c *Campaign) Deploy(manager *account.Account) {
nonce := manager.GetNonce()

gasPrice, err := c.client.SuggestGasPrice(context.Background())
if err != nil {
log.Fatal(err)
}

chainID, err := c.client.ChainID(context.Background())
if err != nil {
log.Fatal(err)
}

auth, err := bind.NewKeyedTransactorWithChainID(manager.PrivateKey, chainID)
if err != nil {
fmt.Println(err)
}
auth.Nonce = big.NewInt(int64(nonce))
auth.Value = big.NewInt(0) // in wei
auth.GasLimit = uint64(300000) // in units
auth.GasPrice = gasPrice

txAddress, tx, instance, err := contracts.DeployCampaign(auth, c.client, c.Min)
if err != nil {
fmt.Println("DeployLottery")
log.Fatal(err.Error())
}

c.instance = instance
c.txAddress = txAddress
c.tx = tx

fmt.Println(txAddress.Hex())
fmt.Println(tx.Hash().Hex())

}

Run

go run *.go

connect to network ...
balance manager : 999999400000000000000
balance provider : 1000000000000000000000
contributor manager : 1000000000000000000000
nonce manager : 1
deploying contract ... _____________________
0x8195b8a7757977AA6ae6Df873dA452a65Ab4791F
0x1e75f1dfa1a7454599acce94e4e1ccfeaa4d9633e0e11ec75cc896c0981e77bb
balance manager : 999998800000000000000

Terminal

  Transaction: 0x1e75f1dfa1a7454599acce94e4e1ccfeaa4d9633e0e11ec75cc896c0981e77bb
Contract created: 0x8195b8a7757977aa6ae6df873da452a65ab4791f
Gas usage: 300000
Block number: 2
Block time: Wed Dec 27 2023 16:40:48 GMT
Runtime error: code size to deposit exceeds maximum code size

Now we deploy our contract on Ganache network.

Get Information from the block

func (a *Account) GetHeader() *big.Int {

header, err := a.client.HeaderByNumber(context.Background(), nil)
if err != nil {
log.Fatal(err)
}

fmt.Println(header.Number.String()) // 5671744

return header.Number
}

func (a *Account) GetDataFromBlock(blockNumber *big.Int) {
block, err := a.client.BlockByNumber(context.Background(), blockNumber)
if err != nil {
log.Fatal(err)
}

fmt.Println("block.Number().Uint64() ", block.Number().Uint64()) // 5671744
fmt.Println("block.Time()", block.Time()) // 1527211625
fmt.Println("block.Difficulty().Uint64()", block.Difficulty().Uint64()) // 3217000136609065
fmt.Println("block.Hash().Hex()", block.Hash().Hex()) // 0x9e8751ebb5069389b855bba72d94902cc385042661498a415979b7b6ee9ba4b9
fmt.Println("len(block.Transactions())", len(block.Transactions())) // 144
}

One time deploy and then only use the contract transaction

 my := common.HexToAddress("0xdd3f3c8afb70786e3bfb7a8cd8d44a3c5049268e")

var err error
campaign.Instance, err = contracts.NewCampaign(my, client.Client)
if err != nil {
fmt.Println(err)
}

Contribute on network

func (c *Campaign) Contribute(contributor *account.Account) {

nonce := contributor.GetNonce()

gasPrice, err := c.client.SuggestGasPrice(context.Background())
if err != nil {
log.Fatal(err)
}

chainID, err := c.client.ChainID(context.Background())
if err != nil {
log.Fatal(err)
}

auth, err := bind.NewKeyedTransactorWithChainID(contributor.PrivateKey, chainID)
if err != nil {
log.Fatal(err)
}
auth.Nonce = big.NewInt(int64(nonce))
auth.Value = c.Min // in wei
auth.GasLimit = uint64(3000000) // in units
auth.GasPrice = gasPrice

tx, err := c.instance.Contribute(auth)
if err != nil {
log.Fatal(err)
}

fmt.Println("transaction hash")
fmt.Println(tx.Hash())

}
go run *.go


connect to network ...

block.Number().Uint64() 21
block.Time() 1703691148
block.Difficulty().Uint64() 0
block.Hash().Hex() 0x06c93b2954bed6bd1f76340b4b504c985293abbff6b21006dc359daf0d65198c
len(block.Transactions()) 1

>>>>>>>>>>>>>>>>>>>>>>Balance>>>>>>>>>>>>>>>>
balance manager : 999992200000000000000
balance provider : 1000000000000000000000
balance contributor : 999999662975999994800
<<<<<<<<<<<<<<<<<<<<<<Balance<<<<<<<<<<<<<<<<<

deploying contract ...
0xaf9087a308a23E335Dd8bB73E5b77AD0bF625Cd0
0xd56b10c190f65cec3be4f437f22da9fce4f3149d770b65fb494ca5f79af70d48


>>>>>>>>>>>>>>>>>>>>>>Balance>>>>>>>>>>>>>>>>
balance manager : 999991600000000000000
balance provider : 1000000000000000000000
balance contributor : 999999662975999994800
<<<<<<<<<<<<<<<<<<<<<<Balance<<<<<<<<<<<<<<<<<

contribute ...
transaction hash ...
0xfc2028dbaa4235e4a19a0b2e437db48b8cdbb6e05f0bebdd93eda4209e0541c6


>>>>>>>>>>>>>>>>>>>>>>Balance>>>>>>>>>>>>>>>>
balance manager : 999991600000000000000
balance provider : 1000000000000000000000
balance contributor : 999999620847999993800
<<<<<<<<<<<<<<<<<<<<<<Balance<<<<<<<<<<<<<<<<<

Console result:

deploy

  Transaction: 0xd56b10c190f65cec3be4f437f22da9fce4f3149d770b65fb494ca5f79af70d48
Contract created: 0xaf9087a308a23e335dd8bb73e5b77ad0bf625cd0
Gas usage: 300000
Block number: 22

contribute

  Transaction: 0xfc2028dbaa4235e4a19a0b2e437db48b8cdbb6e05f0bebdd93eda4209e0541c6
Gas usage: 21064
Block number: 23

Call Is paid function

func (c *Campaign) IsPaid(contributor *account.Account) bool {

result, err := c.Instance.IsPaid(&bind.CallOpts{
From: contributor.PublicAddress,
Context: context.Background(),
})
if err != nil {
fmt.Println("IsPaid")
fmt.Println(err)
}

fmt.Println(result)

return result
}

Call Get balance function

func (c *Campaign) GetBalance() *big.Int {
result, err := c.Instance.GetBalance(nil)
if err != nil {
fmt.Println("IsPaid")
fmt.Println(err)
}

fmt.Println(result)

return result
}

Add request

func (c *Campaign) AddRequest(request entity.Request, manager *account.Account) {
nonce := manager.GetNonce()

gasPrice, err := c.client.SuggestGasPrice(context.Background())
if err != nil {
log.Fatal(err)
}

auth, err := bind.NewKeyedTransactorWithChainID(manager.PrivateKey, c.chainID)
if err != nil {
log.Fatal(err)
}
auth.Nonce = big.NewInt(int64(nonce))
auth.Value = big.NewInt(0) // in wei
auth.GasLimit = uint64(6721975) // in units 6721975
auth.GasPrice = gasPrice

tx, err := c.Instance.AddRequest(auth, request.Description, request.Value, request.ProviderAddress)
if err != nil {
fmt.Println("CreateRequest")
fmt.Println(err.Error())
log.Fatal(err)
}

fmt.Println("transaction hash ...")
fmt.Println(tx.Hash())

}

Function get number of requests

func (c *Campaign) GetNumberOfRequests() *big.Int {
result, err := c.Instance.NumRequests(nil)
if err != nil {
fmt.Println("GetNumberOfRequests")
fmt.Println(err)
}

fmt.Println(result)

return result
}

Function Approve request

func (c *Campaign) ApproveRequest(contributor *account.Account, index *big.Int) {

nonce := contributor.GetNonce()

gasPrice, err := c.client.SuggestGasPrice(context.Background())
if err != nil {
log.Fatal(err)
}

auth, err := bind.NewKeyedTransactorWithChainID(contributor.PrivateKey, c.chainID)
if err != nil {
log.Fatal(err)
}
auth.Nonce = big.NewInt(int64(nonce))
auth.Value = big.NewInt(0) // in wei
auth.GasLimit = uint64(6721975) // in units 6721975
auth.GasPrice = gasPrice

tx, err := c.Instance.ApproveRequest(auth, index)
if err != nil {
fmt.Println("ApproveRequest")
fmt.Println(err.Error())
log.Fatal(err)
}

fmt.Println("transaction hash ...")
fmt.Println(tx.Hash())
}

Function Finalize Request

func (c *Campaign) FinalizeRequest(manager *account.Account, index *big.Int) {
nonce := manager.GetNonce()

gasPrice, err := c.client.SuggestGasPrice(context.Background())
if err != nil {
log.Fatal(err)
}

auth, err := bind.NewKeyedTransactorWithChainID(manager.PrivateKey, c.chainID)
if err != nil {
log.Fatal(err)
}
auth.Nonce = big.NewInt(int64(nonce))
auth.Value = big.NewInt(0) // in wei
auth.GasLimit = uint64(6721975) // in units 6721975
auth.GasPrice = gasPrice

tx, err := c.Instance.FinalizeRequest(auth, index)
if err != nil {
fmt.Println("FinalizeRequest")
fmt.Println(err.Error())
log.Fatal(err)
}

fmt.Println("transaction hash ...")
fmt.Println(tx.Hash())
}

Final code

--

--