Skip to content

Contract를 학습한다.(clone: crowdfunding) #2

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 21 commits into
base: develop
Choose a base branch
from

Conversation

4BFC
Copy link
Member

@4BFC 4BFC commented Jan 16, 2025

환경 설정

contract를 개발하기 위해서는 여러 환경을 세팅하는 방법이 있다. 대표적으로 Truffle과 Remix IDE가 있다. Truffle같은 경우에는 Linux 환경에서 최적화 되어 있는 환경이기 때문에 이를 제외하고 Remix IDE를 VSC와 연동해서 contract를 개발하는 방법을 먼저 소개 하겠다.
우선 vsc에서 아래와 같은 명령어를 실행한다.

remixd -s . --remix-ide https://remix.ethereum.org

그러면 서버가 동작을 하는데 이때, Remix IDE에 접속해서 Connect to Local Filesystem을 클릭한다. 그러면 자연스럽게 vsc환경에 Remix IDE와 동일한 환경이 제공된다. 여기서 만약 방화벽 관련 Error가 발생을 하면 Remix IDE의 주소에서는 방화벽을 끌 수 있게 브라우저 환경을 세팅해야 한다. 그렇게 개발을 하고 compiledeploy는 Remix IDE에서 실행하면 된다. 단, 사용을 해보니 해당 환경은 contract를 개발하기에는 편리하나 계속 Remix IDE를 신경쓰고 이와 연동을 유지하기 위해서 여러 제약 조건이 따르기 때문에 조금은 불편하다 느꼈다. 그래서 이를 대체하기 위해서 다음과 같은 SDK를 사용하기로 했다.

ThirdWeb.js

해당 SDK는 contract 관리부터 contract를 컨트롤 할 수 있는 환경을 제공해주는 SDK이다. 쉽게 말해서 supabase와 비슷한 개념이라 생각하면 된다. 여기서 개발을 하고 deploy를 하면 빠르게 나의 contract를 볼 수 있고 이를 관리 할 수 있다. thirdweb으로 개발할 수 있는 환경을 세팅하는 방법은 다음과 같다.

npx thirdweb@latest create --contract  // thirdweb으로 contract 개발 환경 설정
// thirdweb에서 project를 만들어 주고 여기서 thirdweb에서 제공해주는 `secret key`를 잊지 않고 저장해야 한다.
npx thirdweb@latest deploy -k THIRDWEB_SECRET_KEY  // deploy 실행 방법 cross-env를 설정했지만 결과는 좋지 않았다.

생각 보다 간단하고 작업환경을 만들기가 어렵지 않았다. 이제 contract를 살펴보자.

Contract

Campaign 설정 및 기본 제공, 필요한 데이터

우리 contract가 사용될 목적과 방향은 다음과 같다. 캠페인을 통해서 기부할 수 있는 기능을 제공하려 한다. 그러기 위해서는 캠페인에 필요한 정보들부터 구성해야 한다. 해당 코드는 다음과 같다. (기본 문법은 숙지가 되었다는 전제로 설명할 예정이다.)

struct Campaign {
    address owner;          // 캠페인 생성자의 지갑 주소
    string title;           // 캠페인 제목
    string description;     // 캠페인 설명
    uint256 target;         // 목표 금액
    uint256 deadline;       // 마감 기한 (timestamp)
    uint256 amountCollected;// 모금된 총 금액
    string image;           // 캠페인 이미지 URL
    address[] donators;     // 기부자들의 지갑 주소 목록
    uint256[] donations;    // 각 기부자가 기부한 금액
}

이렇게 구조가 짜여져 있고 우리가 제공, 제공 받을 캠페인을 저장할 상태 변수를 만들어 줘야 한다.

  mapping(uint256 => Campaign) public campaings;
  uint256 public numberOfCampaigns = 0;

코드를 보면 Campaign을 unit256 데이터 타입으로 바인딩을 해서 campaigns 변수로 정의를 한 것이다. 그렇게 key(uint256), value(Campaign)로 구성 한 것이다. 여기서 특이한 점은 uint256인 key값을 따로 0으로 정의하지 않았다. 이유는 solidity에서 mapping은 key가 0으로 자동 설정되기 때문이다. numberOfCampaigns은 Campaign가 생성되면서 생성된 구조체의 index값을 표현하기 위해서 정의된 값이다.

Campaign 생성하기 (createCampaign)

이제 기본적인 campaign에 필요한 정보를 담고 추적할 수 있는 환경은 얼추 만들어 졌다. 이제는 생성하고 데이터를 저장해야 하는데 이를 구현한 코드는 다음과 같다.

function createCampaign(
    address _owner, 
    string memory _title, 
    string memory _description, 
    uint256 _target, 
    uint256 _deadline, 
    string memory _image
) public returns(uint256) {

<-- 아래 참조 생략 -->

우선 함수의 매개변수로 _owner, _title, _description, _target, _deadline, _image들로 이루어 져있다. 이것들은 campaign을 생성할 때, 필요한 정보들을 바탕으로 구성 되어 있다. 이제 클라이언트가 위 함수에 접근하고 해당 데이터를 모두 입력하고 승인을 했다면 해당 데이터가 저장 되어야 한다. 코드는 다음과 같다.

  Campaign storage campaign = campaings[numberOfCampaigns];
  require(campaign.deadline < block.timestamp, "The deadline should be a date in the future.");

우리가 앞서 정의한 index값으로 새로 생성된 campaings를 Campaign 구조체(데이터)타입으로 선언한 단수형 campaign에 저장한다.

storage

여기서 storage는 Solidity의 데이터 위치(Data Location)를 지정하는 키워드이다. Solidity에서는 데이터를 저장할 때 데이터의 저장 위치를 반드시 명시해야 한다. 조금 더 설명하자면 storage는 영구 저장(블록체인)이 되고 memory는 함수 실행 중에만 임시적으로 저장하게 된다. calldata는 읽기 전용 임시 저장소이며 주로 외부 함수 입력값에 사용된다. 다시 설명하자면 storage는 원본 데이터에 직접적인 영향을 주고 memory는 데이터를 임시 복사 하기 때문에 원본 데이터에 영향을 주지 않는다.

require

require는 if문과 같은 조건문이다. 단, 이 둘의 명확한 차이는 있다. require는 실패 시 트랜잭션을 되돌린다. 즉, 데이터를 원상태로 복구시켜 데이터의 변화를 주지 않는 것이다. require(campaign.deadline < block.timestamp, "The deadline should be a date in the future.");해당 코드를 살펴보면 campaign.deadline이 현재 시간보다 미래인지 확인하는 조건문이다. 이를 통해서 contract의 조건 검사와 에러 처리를 하면서 트랙잭션을 관리한다. block.timestamp는 블록이 채굴된 시간으로, 현재 블록체인의 시간을 의미한다. 그리고 마지막 인자의 메시지 같은 경우 실패한 경우 반환되는 error 메시지이다.

여기까지 함수로부터 numberOfCampaigns으로 선택된 campaings객체에 각 매개변수로 부터 받은 데이터를 저장하고 numberOfCampaigns의 값을 증가 시킨다. 이로인해 numberOfCampaigns의 본래의 값은 증가한다. 여기서 눈여겨 봐야할 것은 numberOfCampaigns가 return 되는데 이유는 우리가 저장한 Campaign의 구조체 index 값을 반환하기 위해서 numberOfCampaigns -1을 반환하는 것이다.

기부 기능 구현 (donateToCampaign)

우리가 기부를 받는 함수를 구현하기 위해서는 다음과 같은 코드로 구현이 되어야 한다.

    function donateToCampaign(uint256 _id) public payable{
        uint256 amount = msg.value;

        Campaign storage campaign = campaings[_id];

        campaign.donators.push(msg.sender);
        campaign.donations.push(amount);

        // 반환값 구조 분해 할당, 첫 번째 호출 성공여부, 두 번째 호출 결과과
        (bool sent,) = payable(campaign.owner).call{value:amount}("");

        if(sent){
            campaign.amountCollected = campaign.amountCollected + amount;
        }
    }

사용자가 캠페인에 기부(ETH)를 할 수 있다. 여기서 msg는 사용자가 요청한 ETH의 양이 된다. (bool sent,) = payable(campaign.owner).call{value:amount}("");해당 코드는 기부받은 금액을 캠패인 생성자에게 전송하게 되고 call은 ETH를 안전하게 전송하는 함수이다.여기서 저수준 함수에서 call의 반환값(tuple) bool의 상태가 sent로 전달이 되는 반환 값이며 sent가 true이면 성공, false이면 실패이다. 이에 맞춰서 반환 받은 sent에 따라서 amountColledted의 값이 요청한 amount만큼 증가 하게 된다.

  • 해당 저수준 함수를 이해해야한다. 흡수 함수 같지만 해당 구조는 반환 값을 받는 방식이다.

기부자 조회 구현 (getDonators)

기부자의 목록과 기부금액을 조회 할 수 있는 간단한 함수이다.

    function getDonators(uint256 _id) view public returns (address[] memory, uint256[] memory){
        return (campaings[_id].donators, campaings[_id].donations);
    }

위 코드를 살펴보면 함수의 매개변수를 통해서 _id값으로 우리가 mapping한 campaings에 해당 id를 조회해서 결과들을 반환한다.

전체 캠페인 조회 함수

현재 존재하는 모든 캠페인을 반환하는 함수이다. 코드는 다음과 같다.

    function getCampaings() public view returns (Campaign[] memory){
        Campaign[] memory allCampaigns = new Campaign[](numberOfCampaigns);

        for(uint i = 0; i < numberOfCampaigns; i++){
            Campaign storage item = campaings[i];
            allCampaigns[i] = item;
        }
        return allCampaigns;
    } 

코드를 살펴보면 우선 Campaign[] memory allCampaigns = new Campaign[](numberOfCampaigns); 동적 배열을 만들어 캠페인을 복사한다. 이후 각 저장되어 있는 값들을 복사한 allCampaigns에 모두 넣어서 이를 반환한다.

이렇게 전반적인 코드를 살펴보았다. 이제 우리는 이 contract를 thirdweb을 통햇 배포를 하고 해당 contract의 상태에 따라 변경이 가능한 함수,변수 들이 있고 그렇지 않은 함수, 변수들이 정해지게 된다. 이 기준은 view, pure 키워드에 따라 상태가 달라진다. 즉, 위 코드에서 우리가 public, private 뒤에 붙는 키워드 view, paybale에 따라서 Read와 Write 상태가 분리되는 것이다. 좀 더 나아가서 해당 상태에 따라 트랜잭션, 가스비 의 유무가 결정되는데 Read는 트랜잭션, 가스비는 전혀 발생하지 않고 반대로 write는 둘 다 비용이 발생하게 된다.

@4BFC 4BFC self-assigned this Jan 16, 2025
@4BFC 4BFC added the 📝Contract 📝Contract label Jan 16, 2025
@4BFC 4BFC changed the title Contract를 학습한다. Contract를 학습한다.(clone: crowdfunding) Jan 16, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
📝Contract 📝Contract
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant