Last active
September 26, 2024 09:57
-
-
Save Violet-Bora-Lee/8399ec45c4051de922162532ebb3f192 to your computer and use it in GitHub Desktop.
Pocket Solidity by Bora Lee 솔리디티 문법 정리, 이보라의 포켓 솔리디티
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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