1996년 닉 사보 (Nick Szabo)가 제안한 "Smart Contracts: Building Blocks for Digital Free Markets" 이라는 논문에서 스마트 컨트랙트의 개념이 생겼다. 이 논문에서 사보는 전자적인 커머스 프로토콜을 이용해서 인터넷에서 서로 모르는 사람들끼리 계약 (Contract Law)을 만들고 계약과 관련된 비즈니스 프랙티스 (business Practice)를 만드는 방법을 기술했고, 이 방법을 스마트 컨트랙트 (Smart Contract)라고 불렀다.
스마트 컨트랙트는 계약 참가자들이 정의된 약속들을 수행하는 프로토콜을 포함하는 프로그램이다. 스마트 컨트랙트는 사람의 개입 없이 프로그램으로 명시된 내용에 따라 계약을 집행하기 때문에 계약 당사자 간에 분쟁이 발생하기 어렵다.
EVM은 이더리움에서 스마트 컨트랙트를 실행하기 위한 실행 환경을 제공한다. 스마트 컨트랙트는 EVM에서 실행 가능한 바이트 코드로 컴파일되고 블록체인에 저장된다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract Greeter {
function sayHello() public pure returns (string memory) {
return "Hello?";
}
}
function addToX(uint y) public view returns (uint) {
// x = 1; 상태 변수를 수정하지 못한다.
return x + y;
}
function addToX(uint y) public view returns (uint) {
// x = 1; 상태 변수를 읽거나 수정하지 못한다.
return x + y;
}
상태 변경성은 함수 제어자 (function modifier)라고도 하는데, 책에서 상태 변경성 (state mutability) 라는 단어를 사용했다. 아래와 같이 에러를 발생해서 확인해보니 에러 메세지에 state mutability 라고 나오는 것을 확인했다.
pragma solidity ^0.8.20;
contract GreeterContract {
mapping (uint8 => Greeting) greetingByLang;
mapping (string => uint8) langMap;
struct Greeting {
string hello;
string goodbye;
}
enum Lang { Korean, English }
constructor () {
greetingByLang[uint8(Lang.Korean)] = Greeting("annyeong?", "bye!");
greetingByLang[uint8(Lang.English)] = Greeting({goodbye: "Goodybye!", hello: "Hello"});
langMap["Korean"] = uint8(Lang.Korean);
langMap["English"] = uint8(Lang.English);
}
function sayHello(string calldata _lang ) public view returns (string memory){
uint8 lang = langMap[_lang];
return greetingByLang[lang].hello;
}
function changeHello(string calldata _lang, string calldata _hello) public {
uint8 lang = langMap[_lang];
greetingByLang[lang].hello = _hello;
}
function sayGoodbye(string calldata _lang ) public view returns (string memory){
uint8 lang = langMap[_lang];
return greetingByLang[lang].goodbye;
}
function changeGoodbye(string calldata _lang, string calldata _goodbye) public {
uint8 lang = langMap[_lang];
greetingByLang[lang].hello = _goodbye;
}
}
책 버전과 변경된 점은 다음과 같다.
1. contract 이름과 동일한 function을 생성할 수 없다.
2. 생성자 함수는 function 키워드 붙이지 않고 가시 지정자 선언을 할 수 없다.
3. 함수 매개변수에서 array, string, mapping과 같은 complex types 형태의 변수는 반드시 data location (storage, memory, calldata)를 명시해야 한다.
memory와 calldata의 주요 차이점은 수정이 불가능하다. memory는 데이터에 대한 read, write가 가능하지만 calldata는 readonly 형태를 가진다. 따라서, calldata는 함수의 return 매개변수로 반환할 수 없다. 함수 매개변수로 memory를 입력받을 때 동작 과정은 다음과 같다.
1. 매개변수로 입력받은 데이터가 calldata 형태로 입력받는다.
2. memory를 만들어서 calldata의 데이터를 복사하여 저장한다.
만약, memory가 아닌 calldata로 받는다면 2번 과정인, memory를 생성해서 데이터를 복사하는 과정이 생략된다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract MinimumViableToken {
mapping (address => uint256) public balanceOf;
constructor (uint256 initialSupply) {
balanceOf[msg.sender] = initialSupply;
}
function transfer(address _to, uint256 _value) public {
balanceOf[msg.sender] -= _value; balanceOf[_to] += _value;
}
}
ERC-20 Standard Method
function name() public view returns (string)
function symbol() public view returns (string)
function decimals() public view returns (uint8)
function totalSupply() public view returns (uint256)
function balanceOf(address _owner) public view returns (uint256 balance)
function transfer(address _to, uint256 _value) public returns (bool success)
function transferFrom(address _from, address _to, uint256 _value) public returns (bool success)
function approve(address _spender, uint256 _value) public returns (bool success)
function allowance(address _owner, address _spender) public view returns (uint256 remaining)
ERC-20에 명시되어 있는 기본 요구사항을 충족하면 ERC-20 호환 토큰이 된다.
ERC-20 참고자료: https://velog.io/@jhcha/Solidity-ERC-20
마찬가지로 비정상적인 오류를 통해 return하는 경우, 단순 함수의 동작만 종료하는 것이 아닌 revert같은 트랜잭션 오류 처리를 사용해야 한다.
revert, throw는 모두 해당 트랜잭션에서 발생한 모든 상태 변경을 무효화하고 트랜잭션 시작 이전 상태로 되돌리기 위한 명령이다.
require, revert, assert를 호출하여 에러를 발생시킬 수 있다.
function testRequire(uint _i) public pure {
// 입력 값이 10 이하인 경우, 에러 메세지를 반환하고 종료
require(_i > 10, "Input must be greater than 10");
}
function testRevert(uint _i) public pure {
// 입력 값이 10 이하인 경우
if (_i <= 10) {
//에러 메세지를 반환하고 종료
revert("Input must be greater than 10");
}
}
function multiple(uint num1, uint num2) public pure returns (uint) {
require(num1 % 2 == 0 || num2 % 2 == 0, "input error");
uint result = num1 * num2;
assert(result % 2 == 0);
return result;
}
require, revert는 오류 발생 시 가스를 사용하지 않고 종료된다. 하지만, assert는 가스를 모두 소모한 후 종료된다.
영화 제작을 위한 크라우드 펀드 컨트랙트 요구사항은 다음과 같다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
interface token {
function transfer(address receiver, uint amount) external;
}
contract CrowdFundContract {
address public beneficiary;
uint public fundingGoal;
uint public amountRaised;
uint public deadline;
uint public price;
token public tokenReward;
mapping(address => uint256) public balanceOf;
bool public fundingGoalReached = false;
bool public crowdsaleClosed = false;
event GoalReached(address beneficiaryAddress, uint amountRaisedValue);
event FundTransfer(address backer, uint amount, bool isContribution);
constructor (
address ifSuccessfulSendTo,
uint fundingGoalInEthers,
uint durationInMinutes,
uint etherCostOfEachToken,
address addressOfTokenUsedAsReward
) {
beneficiary = ifSuccessfulSendTo;
fundingGoal = fundingGoalInEthers * 1 ether;
deadline = block.timestamp + durationInMinutes * 1 minutes;
price = etherCostOfEachToken * 1 ether;
tokenReward = token(addressOfTokenUsedAsReward);
}
fallback () external payable{
require(!crowdsaleClosed);
uint amount = msg.value;
balanceOf[msg.sender] += amount;
amountRaised += amount;
tokenReward.transfer(msg.sender, amount / price);
emit FundTransfer(msg.sender, amount, true);
}
modifier afterDeadline() { if (block.timestamp >= deadline) _; }
function checkGoalReached() external afterDeadline {
if (amountRaised >= fundingGoal){
fundingGoalReached = true;
emit GoalReached(beneficiary, amountRaised);
}
crowdsaleClosed = true;
}
function safeWithdrawal() external afterDeadline {
if (!fundingGoalReached) {
uint amount = balanceOf[msg.sender];
balanceOf[msg.sender] = 0;
if (amount > 0) {
if (payable(msg.sender).send(amount)) {
emit FundTransfer(msg.sender, amount, false);
} else {
balanceOf[msg.sender] = amount;
}
}
}
if (fundingGoalReached && beneficiary == msg.sender) {
if (payable(beneficiary).send(amountRaised)) {
emit FundTransfer(beneficiary, amountRaised, false);
} else {
fundingGoalReached = false;
}
}
}
}
// is msg.data empty?
// / \
// yes no
// / \
// receive() exists? is the function selector fundMe()?
// / \ / \
// yes no no yes
// / \ / \
// receive() fallback() exists? fundMe()
// / \
// yes no
// / \
// fallback() transaction is reverted
메세지 없이 value만 전송하는 경우 => receive
메세지와 value를 같이 전송하는 경우 => payable function
fallback은 payable function이 없거나, receive가 없는 경우 default payable function으로 기능한다.
참고자료: 박재현, 오재훈, 박혜영. 『코어 이더리움 프로그래밍』. 제이펍(2020). p161-276.