Hiring Assessment 4 - BEB 1기

Terry·2021년 12월 13일
0

Repository: https://github.com/BuilderBoba/Hiring-Assessment-4-BEB-01


Unit 3 - 관리자가 한명인 컨트랙트를 컨트랙트로 변경

Contract Address: 0x518c4ecf6aDAc77C7FE098071fd7df2c2A1BFaa5

https://ropsten.etherscan.io/address/0x518c4ecf6aDAc77C7FE098071fd7df2c2A1BFaa5

이 스마트 컨트랙트는 1-3명의 관리자의 Consensus로 실행 가능한 함수를 표현할 수 있는 컨트랙트이다. 이 Consensus Mechanism은 OwnerHelper sub-contract에 내포되어 있다.

처음 스마트 컨트랙트를 실행할때는 컨트랙트 생성자가 genesisOwner가 된다.

이후 genesisOwner은 1명 혹은 2명의 추가적인 관리자의 주소를 addGenesisOwners(address) 함수를 통해서 _owners 행렬에 저장할 수 있다.

컨트랙트 내에 있는 함수들은 보안용으로 세가지 검증용 modifier을 추가할 수 있다.

세가지 modifier는:
isGenesisOwner() - 컨트랙트 생성자만 실행가능
onlyOwner() - 1-3명의 _owners 주소들만 실행가능
agreementOnly() - _owners 주소들의 합치된 동의가 있어야만 실행가능

예를 들어 처음 genesisOwner가 addGenesisOwners(address)를 통해서 관지라 주소들을 추가할때 isGenesisOwner()가 modifier로 붙고,

관리잘들만 아무때나 실행할 수 있는 함수는 onlyOwner() 함수가 붙는다.

사실 Consensus Mechanism의 적용은 agreementOnly()에서 발생되는데, 일단 각 관리자 주소의 agreement 정보는 AgreementData 자료형(struct)로 저장된다.

struct AgreementData{
        bool yesno;
        string functionCode;
        uint[] parameters;
        string comment;
    }

AgreementData[3] internal ownerAgreements;

그리고 ownerAgreements라는 공간 3짜리 배열안에 관리자들의 agreement 정보가 들어간다.

AgreementData 안에는 4가지 정보가 들어간다:

bool yesno => 최종적으로 동의하는지 안하는지

string functionCode => 어떤 함수에 대해서 agreement를 논하는건지 알려준다. agreementOnly가 들어가는 특별한 함수들은 전부 고유의 functionCode가 함수 내부에 적혀있다.
    
uint[] parameters => 실행할려는 특정 함수의 세부 입력값도 전원 동의해야한다. 입력값이 필요없으면 빈배열 ([])을 넣으면 되고 있다면 그 값들을 배열 안에 넣으면 된다. 아쉽게도 필자는 아직 초보라 Consensus를 요구하는 함수들의 입력값은 정수여야 한다.
    
string comment => Consenssus 작용에 직접적인 영향을 안미치는 선택적인 항이다. 다른 관리자들에게 자신의 의견을 추가하고 싶을때 사용된다.

이제 agreementOnly()의 코드를 보자.

 modifier agreementOnly(){
        if (_numOwners == 2){
            require(bytes(ownerAgreements[0].functionCode).length > 0 && bytes(ownerAgreements[1].functionCode).length > 0, "Agreement not set");
            require(ownerAgreements[0].yesno == ownerAgreements[1].yesno, "Opinions Do Not Match");
            require(keccak256(bytes(ownerAgreements[0].functionCode)) == keccak256(bytes(ownerAgreements[1].functionCode)), "Functions Do Not Match");
            require(ownerAgreements[0].parameters.length == ownerAgreements[1].parameters.length, "Parameter size do not match");

            for (uint i=0; i<ownerAgreements[0].parameters.length; i++){
                require(ownerAgreements[0].parameters[i] == ownerAgreements[1].parameters[i], "Function Parameters Do Not Match");
            }
        } else if (_numOwners == 3){

            require(bytes(ownerAgreements[0].functionCode).length > 0 && bytes(ownerAgreements[1].functionCode).length > 0
            && bytes(ownerAgreements[2].functionCode).length > 0, "Agreement not set");

            require(ownerAgreements[0].yesno == ownerAgreements[1].yesno && 
            ownerAgreements[0].yesno == ownerAgreements[2].yesno, "Opinions Do Not Match");
            
            require(keccak256(bytes(ownerAgreements[0].functionCode)) == keccak256(bytes(ownerAgreements[1].functionCode)) && 
            keccak256(bytes(ownerAgreements[0].functionCode)) == keccak256(bytes(ownerAgreements[2].functionCode)), "Functions Do Not Match");
            
            require(ownerAgreements[0].parameters.length == ownerAgreements[1].parameters.length
            && ownerAgreements[0].parameters.length == ownerAgreements[2].parameters.length, "Parameter size do not match");

            for (uint i=0; i<ownerAgreements[0].parameters.length; i++){
                require(ownerAgreements[0].parameters[i] == ownerAgreements[1].parameters[i], "Function Parameters Do Not Match");
                require(ownerAgreements[0].parameters[i] == ownerAgreements[2].parameters[i], "Function Parameters Do Not Match");
            }
        }
        _;
    }

간단하게 설명하자면 모든 _owners 안에 존재하는 주소들은 서로의AgreementData의 yesno, functionCode, 그리고 parameters가 동일해야한다.

functionCode 같은 string 변수는 솔리디티 안에서 == 매칭이 안되기 때문에 keccak256(bytes(string))을 이용해서 == 처리했다.


이제 agreementOnly가 내재된 함수 실행을 할려면 어떻게 하는지 보자.

 function testTwo() onlyOwner agreementOnly public view returns (string memory){
        string memory testTwoCode = "B4";
        require(keccak256(bytes(ownerAgreements[0].functionCode)) == keccak256(bytes(testTwoCode)), "Consensus reached, but wrong function call");
        return "Test Two: multi-sig consensus success";
    }

함수 testTwo는 입력값이 없는 간단한 함수다.
agreementOnly가 성립되고, 성립하는 agreement가 해당 함수의 고유 functionCode인 "B4"이면 함수 실행이 가능하다.

컨트랙트의 관리자들(_owners)은 해당 testTwo 함수를 실행할려면 addAgreement() 함수를 통해서 자신들의 agreement를 전달할 수 있다. 이 중에서 agreement가 합치하려면 yesno, functionCode, decision 값이 전부 일치해야한다. comment는 전달하고픈 메시지를 적을수 있다.

이렇게 getAgreements로 3명의 조건이 모두 합치해야한다.
yesno는 true, functionCode는 B4, parameters는 세부 입력값이 필요 없으므로 [].

이러면 모든 Consensus가 합치하므로 testtwo함수를 실행하면 성공적으로 실행이 된다!

성공!

하지만 만약 3번 관리자가 이에 동의하지 않으면 어떻게 될까요?

그러면 아래처럼 서로 의견이 안맞아서 오류가 뜨고 함수 실행이 안됩니다.


Unit 4 - Truffle을 이용한 ERC-721 개발

깃헙에서 중요한 부분은

Unit_Four/migrations/2_deploy.js
Unit_Four/truffle-config.js
Unit_Four/scampunks_images
Unit_Four/scampunks_metadata

정도 되겠습니다.

NFT Contract Address: 0x1fa9707f1c172A1601A34A887cC37180d76B0acf

https://rinkeby.etherscan.io/address/0x1fa9707f1c172A1601A34A887cC37180d76B0acf

이 부분 좀 망했는데 그래도 결과물 올립니다 ㅠㅠ

본래 필자의 계획은 메타데이터가 연동된 ScamPunks 라는 NFT 컨트랙트를 Truffle과 OpenZeppelin으로 개발하고 Rinkeby Test Network에 배포를 하여 NFT 민트 후에 민트된 NFT를 Opensea Testnet으로 볼려고 하였다.

우선 메타데이터를 생성하기 위해서 CryptoPunks의 이미지 3개를 가져와서 Pinata라는 IPFS 파일 매니저를 통하여 올렸다.

또한 각 CryptoPunks 이미지마다 해당하는 메타데이터를 .json파일로 하나씩 만들어서 메타데이터 안에 이미지의 링크를 포함시켰다.


https://gateway.pinata.cloud/ipfs/QmU8Jx2NM3xsELjujt3sLY5abh7rpwjGnGPhS517AGhtBc/0.json

이렇게 그냥 링크만 누르면 메타데이터가 있는 곳으로 향하니까 편하다.

그후 ScamPunks라는 폴더를 만들고
1. truffle init
2. npm init -y
3. npm install @openzeppelin/contracts
4. npm install @truffle/hdwallet-provider

를 터미널에서 실행시켜 기본 패키지들을 설치했다.

나는 openzeppelin/contracts 안에 기본으로 제공되는
ERC721PresetMinterPauserAutoId.sol 을 이용해서
간단하게 토큰이름, 토큰심볼, 토큰URI를 입력값으로 넣고 나만의 NFT 컨트랙트를 만들려고 했다.

// migrations/2_deploy.js
// SPDX-License-Identifier: MIT
const ERC721PresetMinterPauserAutoId = artifacts.require(
  "ERC721PresetMinterPauserAutoId"
);

module.exports = function (deployer) {
  deployer.deploy(
    ERC721PresetMinterPauserAutoId,
    "ScamPunks",
    "SP",
    "https://gateway.pinata.cloud/ipfs/QmU8Jx2NM3xsELjujt3sLY5abh7rpwjGnGPhS517AGhtBc/"
  );
};

이렇게 배포할 컨트랙트를 작성한 후 truffle-config.js에서 Rinkeby network에 접속하게끔 필요한 정보를 수정하였다.


여기 보이는 니모닉은 그냥 HA용으로 만들었으니 걱정말라.


이후 나는 rinkeby로 연결된 truffle 콘솔에서 migrate를 실행하였고, 내가 작성한 컨트랙트가 성공적으로 배포됬다.

여기서부터 첫번째 문제가 발생한다.
이더스캔에서 검증용 소스코드를 첨부하는 방법을 몰랐다. 아시다시피 나의 스마트 컨트랙트 구조는 OpenZeppelin을 이용해서 만든거라, 대다수의 functionalities들이 import된 상황이였다.

이더스캔 입장에서는 OpenZeppelin이 뭔지, 요구하는 패키지들이 뭔지 전혀 알 수 없기 때문에 순수 코드를 첨부해야하는데 그걸 못해서 이더스캔 내에서 함수 구현을 할 수 없었다.

그래서 mint 함수의 작동은 나의 truffle 콘솔을 이용해서 실행할 수 밖에 없었다.

자 이렇게 민트 함수를 실행했으면 성공적으로 나의 OpenSea 프로파일 내부에 올바르게 표현된 메타데이터와 이미지가 있기를 기대했다.

그러나....

ScamPunks 이름값 하네요 ^^ (이정도로 먹튀 할려는건 아니였는데...)

네 여기서 두번째 문제가 발생했습니다.

올려놓은 메타데이터가 전혀 반영이 되지 않은 NFT가 배포되었습니다.

일단 문제점이 뭐였냐면 ERC721PresetMinterPauserAutoId.sol 에서 URI를 참조할때는 기본값으로 baseURI + tokenID 를 참조합니다.

예를들어 baseUri가

https://storage.com/BEB/Scampunks/

이고 tokenID가 11 이거나 300이면,

참조되는 URI는 각각

https://storage.com/BEB/Scampunks/11
https://storage.com/BEB/Scampunks/300

이렇게 됩니다.

참 애석하게도 저의 메타데이터 링크는 끝에 .json 이 포함됩니다.

https://gateway.pinata.cloud/ipfs/QmU8Jx2NM3xsELjujt3sLY5abh7rpwjGnGPhS517AGhtBc/ 0.json
https://gateway.pinata.cloud/ipfs/QmU8Jx2NM3xsELjujt3sLY5abh7rpwjGnGPhS517AGhtBc/ 1.json
https://gateway.pinata.cloud/ipfs/QmU8Jx2NM3xsELjujt3sLY5abh7rpwjGnGPhS517AGhtBc/ 2.json

이렇게 말이죠...

그래서 URI 값을 리턴하는 함수에 가서 센스있게 .json 을 붙여보자! 해서 이렇게 작성한 후 컨트랙트를 배포해봤는데도 실패를 하네요 ㅎㅎ...

이번 주말에 꼭 풀어봐야할 두가지 문제인것 같습니다.


회고

음 생각보다 어려운 부분이 많아서 난처했던 시간이 많았던것 같습니다. Remix라는 통합 환경에서 개발할때는 모든 편의 도구들이 제공되었을때의 느낌이 적응됬는지 막상 truffle로 직접 테스트넷 배포 및 개발도구 연동을 해야할때 많은 문제가 생기더군요.

그렇기 때문에 truffle을 활용한 NFT 개발 도중에 대부분의 문제들이 발생했던 것 같습니다.

솔리디티를 배우다보면 "아 이건 참 편리하다" 라는 부분이 생길때도 있지만 "와 이 간단한걸 이렇게 어렵게 해야되" 라는 생각이 들때도 있는 참 흥미진진한(?) 언어인것 같습니다.

openZeppelin의 컨트랙트 소스코드를 응용하면 웬만한 스마트 컨트랙트 개발이 가능해서 참 편리하다고 느꼇지만, 그러면 또 이더스캔에 소스코드 첨부할때 당황하게 되는거 같아요.

또한 솔리디티 언어에서는 string을 활용한 기본 연산이 없다는 것이 정말 너무x10000 불편했던것 같습니다.

그냥 "hi" == "hi" 하면 편할것을 문자열을 바이트 자료형으로 변환해서 그걸 sha256 해쉬로 변환해서 서로 비교해야 한다는게 참 ㅠㅠ

아무튼 그래도 좋은 경험이였습니다!

profile
블록체인 개발 입문자

0개의 댓글

관련 채용 정보