Skip to content

Instantly share code, notes, and snippets.

@Violet-Bora-Lee
Last active September 26, 2024 09:57
Show Gist options
  • Save Violet-Bora-Lee/8399ec45c4051de922162532ebb3f192 to your computer and use it in GitHub Desktop.
Save Violet-Bora-Lee/8399ec45c4051de922162532ebb3f192 to your computer and use it in GitHub Desktop.
Pocket Solidity by Bora Lee 솔리디티 문법 정리, 이보라의 포켓 솔리디티
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;
// 매핑(Mapping)은 해시맵, 객채, 딕셔너리처럼 키-값 쌍이 있는 데이터를 저장할 때 사용한다.
contract Mapping {
// 주소(키)와 uint(값)쌍을 매핑함
mapping(address => uint) public myMap;
function get(address _addr) public view returns (uint) {
// 매핑은 항상 값을 반환하는데,
// 키에 해당하는 값이 없는 경우 기본 값이 반환된다.
// uint의 기본 값은 0이다.
return myMap[_addr];
}
function set(address _addr, uint _i) public {
// address에 해당하는 값을 업데이트한다.
myMap[_addr] = _i;
}
function remove(address _addr) public {
// 값을 기본 값으로 리셋한다.
delete myMap[_addr];
}
}
// 중첩 매핑
// 매핑에서 값이 또 다른 매핑인 경우를 중첩 매핑이라고 한다.
contract NestedMappings {
// 주소와 uint(키)-bool(값)을 쌍으로 가진 매핑을 매핑함
mapping(address => mapping(uint => bool)) public nestedMap;
function get(address _addr1, uint _i) public view returns (bool) {
// 중첩 매핑에선 초기화 하지 않은 값도 가져올 수 있다.
// bool 타입 값의 기본 값은 false이다.
return nestedMap[_addr1][_i];
}
function set(
address _addr1,
uint _i,
bool _boo
) public {
nestedMap[_addr1][_i] = _boo;
}
function remove(address _addr1, uint _i) public {
delete nestedMap[_addr1][_i];
}
}
// Enumerable(열거 가능)의 약자인 Enum은 사용자 정의 타입으로,
// 사람이 읽을 수 있는 형태의 이름을 모아놓은 상수 집합이다.
// enum은 미리 정의된 몇 가지 값중 하나만 사용하도록 제한할 때 주로 사용한다.
contract Enum {
// 예시에서 사용한 enum은 여러 배송 상태를 나타낸다.
enum Status {
Pending,
Shipped,
Accepted,
Rejected,
Canceled
}
// 타입이 Status인 변수, status를 선언한다.
// enum 변수(status)의 기본값은 enum(Status)의 제일 처음 요소(Pending)이다.
Status public status;
// enum은 내부적으론 uint로 표현되기 때문에
// 아래 함수 get은 항상 uint를 반환한다.
// Pending = 0
// Shipped = 1
// Accepted = 2
// Rejected = 3
// Canceled = 4
// 4보다 큰 값은 반환할 수 없다.
function get() public view returns (Status) {
return status;
}
// enum Status에 uint타입의 값을 입력해 요소를 추가한다.
function set(Status _status) public {
status = _status;
}
// 특정 enum 요소를 다음과 같은 방식으로 업데이트 할 수 있다.
// 예시에선 Canceled enum 값으로 업데이트 했다.
function cancel() public {
status = Status.Canceled; // status는 4가 됨
}
}
// 구조체
// 솔리디티에서 지원하는 기본 타입을 사용해 사용자가 새롭게 정의하는 '사용자 정의 타입'으로
// 연관된 데이터를 묶을 때 유용하다.
contract TodoList {
struct TodoItem {
string text;
bool completed;
}
// TodoItem 구조체 요소로 구성된 배열 선언
TodoItem[] public todos;
function createTodo(string memory _text) public {
// 구조체를 초기화 하는 방법은 총 3가지가 있다.
// 방법 1 - 함수처럼 호출
todos.push(TodoItem(_text, false));
// 방법 2 - key-value 매핑
todos.push(TodoItem({ text: _text, completed: false }));
// Method 3 - 빈 구조체 초기화 후 개별 프로퍼티 할당
TodoItem memory todo;
todo.text = _text;
todo.completed = false;
todos.push(todo);
}
// 구조체를 업데이트 하는 방법
function update(uint _index, string memory _text) public {
todos[_index].text = _text;
}
// completed 토글 함수
function toggleCompleted(uint _index) public {
todos[_index].completed = !todos[_index].completed;
}
}
// view, pure 함수
// - view 함수: state 값을 변경하지 않는 함수
// - 블록체인의 상태를 변경하거나 수정하지 않는다.
// - 그러나 블록체인의 현재 상태를 읽을 수 있다. 예를 들어, 계약의 변수 값을 반환하는 함수는 view로 표시한다.
// - pure 함수: state 값을 변경하지 않는 함수이면서 state 값을 읽지도 않는 함수
// - 블록체인의 상태를 읽거나 변경할 수 없다.
// - 즉, 외부 변수나 데이터에 의존하지 않고 입력 값만을 기반으로 결과를 반환한다.
// - 예를 들어, 두 숫자를 더하는 함수는 pure로 표시한다.
// 이 두 키워드를 사용하는 주요 이유:
// 1. 가스 최적화: view 및 pure 함수는 블록체인의 상태를 변경하지 않기 때문에 실행할 때 가스를 소비하지 않습니다.
// 따라서 이러한 함수를 호출할 때 가스 비용이 절약된다.
// (참고로, view와 pure 함수를 외부에서 호출할 때만 가스를 소비하지 않는다.
// 즉, 웹3 라이브러리나 DApp과 같은 외부 인터페이스를 통해 이러한 함수를 호출할 때는 가스가 발생하지 않는다.
// 반면에, 다른 스마트 컨트랙트 함수 내에서 view나 pure 함수를 호출할 경우에는 일정량의 가스가 소비된다.
// 이는 함수의 로직 실행에 필요한 연산 비용 때문이다.)
// 2. 코드의 명확성: 이 키워드를 사용하면 개발자와 사용자 모두 해당 함수가
// 블록체인의 상태에 어떤 영향을 미치는지 쉽게 이해할 수 있다.
// 3. 보안: 함수가 블록체인의 상태를 변경할 수 없다는 것을 명시적으로 나타내면,
// 예기치 않은 상태 변경이나 부작용을 방지할 수 있다.
contract ViewAndPure {
// state 변수 선언
uint public x = 1;
// view 키워드를 붙여 이 함수는 state 값을 변경하지 않는다는 점을 약속함(단, state 값을 읽을 순 있음)
function addToX(uint y) public view returns (uint) {
return x + y;
}
// pure 키워드를 붙여 이 함수는 state 값을 변경하지도, 읽지도 않는다는 점을 약속함
function add(uint i, uint j) public pure returns (uint) {
return i + j;
}
}
// modifier(영한사전 뜻, (의미를 한정하는) 한정어)
// 함수 호출 전후에 실행할 수 있는 코드.
// 일반적으로 특정 함수에 대한 액세스를 제한하고, 입력 매개변수의 유효성을 검사하고,
// 특정 유형의 공격으로부터 보호하는 등의 용도로 사용한다.
contract Modifiers {
address public owner;
constructor() {
// 컨트랙트 배포자를 컨트랙트의 소유자로 설정함
owner = msg.sender;
}
// 컨트랙트 소유자만 함수를 호출할 수 있는 modifier 만들기
modifier onlyOwner() {
require(msg.sender == owner, "You are not the owner");
// 언더스코어는 modifier 안에서 사용히는 특수 문자로
// modifier를 적용한 함수를 이 시점에서 실행하도록 지시하는 역할을 한다.
// 따라서 onlyOwner modifier가 적용된 함수는 예외처리(require)를 먼저 진행한 후에
// 나머지 코드를 실행한다.
_;
}
// 위에서 선언한 modifier, onlyOwner를 적용한 함수 changeOwner 생성
function changeOwner(address _newOwner) public onlyOwner {
// onlyOwner 첫 줄에 적용한 예외처리를 통과해야 이 지점에 도달할 수 있다.
// 따라서 컨트랙트의 현 소유자만 컨트랙트 소유자를 바꿀 수 있다.
owner = _newOwner;
}
}
// 이벤트
// 블록체인에 로그를 남길 때 사용하는 문법(객체)이다.
// 프론트엔드 등에서 특정 컨트랙트에 대한 로그를 파싱하여 응용하려 할 때 유용하다.
// 블록체인 상태변수보다 저렴한 형태의 저장소이다.
contract Events {
// sender 주소와 메시지에 해당하는 문자열을 기록할 용도의 이벤트 선언
event TestCalled(address sender, string message);
function test() public {
// 이벤트 로깅
emit TestCalled(msg.sender, "Someone called test()!");
}
}
// Payable
// payable(지불가능)을 사용해 함수와 주소를 선언하면 컨트랙트로 ETH를 받을 수 있다.
contract Payable {
// Payable 주소는 transfer 또는 send 함수를 통해 ETH를 보낼 수 있다.
address payable public owner;
// Payable 생성자는 ETH를 받을 수 있다.
constructor() payable {
owner = payable(msg.sender);
}
// 이 컨트랙트에 ETH를 입금할 때 호출할 함수.
// 입금할 ETH가 얼마인지를 적어서 이 함수를 호출하면
// 본 컨트랙트의 잔액이 자동으로 업데이트 된다.
function deposit() public payable {}
// 입금할 ETH가 얼마인지를 적어서 이 함수를 호출하면
// 이 함수는 payable이 아니기 때문에 에러가 발생한다.
function notPayable() public {}
// 이 컨트랙트에서 모든 ETH를 인출할 때 호출할 함수
function withdraw() public {
// 이 컨트랙트에 묶인 ETH가 얼마인지 가져옴
uint amount = address(this).balance;
// 모든 ETH를 컨트랙트 소유자에게 보냄
(bool success, ) = owner.call{value: amount}("");
require(success, "Failed to send Ether");
}
// 이 컨트랙트에 묶인 ETH 중 일부를 특정 주소로 보낼 때 호출할 함수
// _to가 payable로 선언됨
function transfer(address payable _to, uint _amount) public {
(bool success, ) = _to.call{value: _amount}("");
require(success, "Failed to send Ether");
}
}
// 이더리움 보내기
// 스마트 컨트랙트에서 특정 주소로 이더리움을 보내는 방법은 총 3가지가 있다.
// 1. send 함수 사용
// send 함수는 지정된 양의 ETH를 특정 주소로 전송하는 함수이다.
// send는 실행시 2300개의 가스만 소비되는 매우 기본적인 작업을 수행하는(low-level) 함수인데,
// 이 정도 가스는 이벤트를 발생(emit)시킬 수만 있고, 거의 아무런 연산을 수행하지 못한다.
// 그런데 만약 send 함수를 실행했고, 그때 2300보다 더 많은 가스가 필요하면 해당 트랜잭션은 실패한다.
// send 함수는 실패할 때 예외를 발생시키지 않고 거짓을 반환한다.
// 따라서 send 함수를 쓸 땐, 항상 결과를 확인하고 실패는 수동으로 처리해야 한다.
contract SendEther {
function sendEth(address payable _to) public payable {
// sendEth라는 payable 함수를 사용해 자신이 전송받은 ETH를 주어진 주소로 포워딩한다.
uint amountToSend = msg.value;
bool sent = _to.send(amountToSend);
require(sent == true, "Failed to send ETH");
}
}
// 2. call 함수
// call은 ETH를 전송하고 남은 가스도 전송하는 로우 레벨 함수이다.
// send와는 다르게 call을 사용하면 EHT를 전송하고 2300 이상의 가스가 필요한
// 더 복잡한 작업을 수행할 수 있다.
// 다만 ETH 전송에 실패하면 send와 마찬가지로 트랜잭션이 자동으로 취소되진 않는다.
// 대신 call은 send와 다르게 전송에 실패하면 거짓을 반환한다.
// 따라서 call을 쓸땐, 항상 반환값을 확인하고 실패 사례를 수동으로 처리해야 한다.
// 여기에 더하여 call은 남은 가스를 모두 전달하기 때문에 신중하게 사용하지 않으면
// 재진입 공격이 발생할 수 있다는걸 염두해야 한다.
contract SendEther {
function sendEth(address payable _to) public payable {
// 이 payable 함수에서 원하는 주소로 ETH를 보낼 수 있다.
uint amountToSend = msg.value;
// call은 불린값을 반환하는데 반환값 참/거짓은 성공/실패를 나타낸다.
(bool success, bytes memory data) = _to.call{value: msg.value}("");
require(success == true, "Failed to send ETH");
}
}
// 3. transfer 함수
// transfer 함수는 지정된 양의 ETH를 특정 주소로 전송한다.
// transfer 함수는 send 함수와는 다르게 전송 실패시 자동으로 예외가 발생하고
// 트랜잭션 역시 되돌아간다.
// 이런 이유 때문에 transfer 함수를 쓸 땐 수동으로 전송 결과를 확인하거나
// 실패 사례를 처리할 필요가 없으므로 더 안전하고 쉽게 사용할 수 있습니다.
// 하지만 transfer 함수도 send와 마찬가지로 실행시 2300 가스까지 허용된다.
contract SendEther {
function sendEth(address payable _to) public payable {
// 이 payable 함수에서 원하는 주소로 ETH를 보낼 수 있다.
uint amountToSend = msg.value;
// transfer 메서드를 사용해 ETH를 보낸다.
_to.transfer(msg.value);
}
}
// 현황
// 업계 표준으로는 `transfer`와 `send`를 사용하는 것이 일반적이었으나,
// 여러 보안 문제로 인해 현재는 `call` 메서드를 사용하여 보다 안전하게 토큰을 전송하는 것이 권장된다.
// 특히, reentrancy 공격과 같은 보안 위협을 방지하기 위해 call을 사용하면서도 필요한 보안 조치를 취하는 것이 중요하다.
// 이더리움 받기
// 개인 키로 통제 가능한 외부 소유 계정(Externally Owned Account, EOA)은 이더리움 전송을 자동으로 수락할 수 있기 때문에
// 이더리움을 받을 때 특별한 조치를 할 필요가 없다.
// 하지만 이더리움을 직접 받을 수 있는 컨트랙트를 작성하는 경우, 아래 함수 중 하나 이상을 포함해 놓아야 한다.
// - receive() external payable
// - fallback() external payable
// 여기서 receive()는 msg.data가 빈 값이면 호출되고, fallback()은 msg.data가 빈 값이 아닐 때 호출된다.
contract ReceiveEther {
/*
fallback() 또는 receive() 중 무엇이 호출될까?
Ether를 보냄
|
msg.data가 빈 값인가?
/ \
네 아니오
/ \
receive()가 존재하는가? fallback()
/ \
네 아니오
/ \
receive() 호출 fallback() 호출
*/
// ETH를 받기위해 필요한 함수. msg.data가 빈 값이어야 함
receive() external payable {}
// msg.data가 빈 값이 아닌 경우 호출되는 함수
fallback() external payable {}
function getBalance() public view returns (uint) {
return address(this).balance;
}
}
// 다른 컨트랙트 호출하기
// 컨트랙트는 A.foo(x, y, z)와 같이 다른 컨트랙트의 인스턴스에 있는 함수를 호출하는 방식으로
// 자신이 아닌 다른 컨트랙트를 호출할 수 있다.
// 그런데 다른 컨트랙트를 호출하려면 해당 컨트랙트에 어떤 함수가 존재하는지 알려주는 A에 대한 인터페이스가 있어야 한다.
// 솔리디티의 인터페이스는 헤더 파일처럼 작동하며, 프론트엔드에서 컨트랙트를 호출할 때 사용하던 ABI와 비슷한 용도로 사용할 수 있다.
// interface를 통해 컨트랙트는 외부 컨트랙트를 호출시, 함수 인수와 반환값을 인코딩하고 디코딩하는 방법을 알아낸다.
// 참고: 인터페이스엔 외부 컨트랙트에 존재하는 모든 함수에 대한 정보가 들어가지 않아도 된다.
// 언제가 호출 할 함수만 포함해도 괜찮다.
// 외부에 ERC20 컨트랙트가 있고,
// 아래 컨트랙트에선 주어진 주소의 잔액을 확인하기 위해 ERC20 컨트랙트에 있는 balanceOf 함수를 호출하고 싶다고 가정해보자.
interface MinimalERC20 {
// 인터페이스엔 실제 관심있는 함수만 넣는다.
function balanceOf(address account) external view returns (uint256);
}
contract MyContract {
MinimalERC20 externalContract;
constructor(address _externalContract) {
// MinimalERC20 컨트랙트 인스턴스 초기화
externalContract = MinimalERC20(_externalContract);
}
// mustHaveSomeBalance를 호출하는 사람의 ERC20 잔액이 0 이상인지 확인하는 함수
function mustHaveSomeBalance() public {
// ERC20 관련 외부 함수에서 해당 토큰의 잔액 정보를 가져온다.
uint balance = externalContract.balanceOf(msg.sender);
require(balance > 0, "You dont own any tokens of external contract");
}
}
// 컨트랙트 import 하기
// 한 파일에 너무 많은 컨트랙트를 작성하면 코드 가독성이 떨어지므로,
// 여러 파일에 컨트랙트를 나눠 가독성을 올릴 수 있다.
// 다음과 같은 폴더 구조가 있다고 가정해보자.
// ├── Import.sol
// └── Foo.sol
// Foo.sol 파일은 다음과 같다고 가정하자.
contract Foo {
string public name = "Foo";
}
// 이 경우 Import.sol에서 Foo.sol을 다음과 같이 import(가져오기)할 수 있다.
// 옳바른 경로와 함께 Foo.sol파일 가져옴
import "./Foo.sol";
contract Import {
// Foo.sol 초기화
Foo public foo = new Foo();
// Foo.sol에 있는 변수(name)이 잘 가져와지는지 확인
function getFooName() public view returns (string memory) {
return foo.name();
}
}
// 참고: 하드햇을 사용하는 경우, npm을 통해 컨트랙트를 노드 모듈로 설치해서 생성되는
// node_modules 폴더에서 컨트랙트를 가져온다.
// 기술적으로 패키지를 설치할 때 로컬 머신에 컨트랙트가 다운로드되는 것이기 때문에
// 하드햇을 사용하는 방식도 위 방식과 큰 차이가 없다.
// 라이브러리
// library는 contract와 유사하지만 몇 가지 제한 사항이 존재한다.
// 라이브러리는 상태 변수를 포함할 수 없으며 이더를 전송할 수 없다.
// 여기에 더하여 라이브러리는 네트워크에 한 번만 배포되므로
// 다른 사람이 배포한 라이브러리를 사용하는 경우
// 이더리움은 해당 라이브러리가 과거에 이미 다른 사람이 배포한지 알기 때문에
// 내 코드를 배포할 때 가스를 지불할 필요가 없다는 특징이 있다.
library SafeMath {
function add(uint x, uint y) internal pure returns (uint) {
uint z = x + y;
// z가 오버플로우 되었을 때 에러를 던짐
require(z >= x, "uint overflow");
return z;
}
}
contract TestSafeMath {
function testAdd(uint x, uint y) public pure returns (uint) {
return SafeMath.add(x, y);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment