External accounts : public-private key 쌍에 의해 제어됨.
: 주소는 생성된 public-key에 의해 생성됨.
contract accounts : account에 저장된 code에 의해 제어됨.
: 주소는 계약이 생성되는 시간에 의해 생성됨.
모든 계정은 storage (256-bit to 256-bit key-value mapping 저장소)를 가지고 있음.
모든 계정은 balance를 가지고 있음.
Transaction은 payload 와 ether를 포함하는 message임.payload는 입력 데이터로써 작용함.null인 경우) 새 컨트랙트를 생성함. struct 안에서 여러 변수들을 구성하고, 압축하게 된다면 그 변수들이 더 적은 공간을 차지하도록 솔리디티가 최적화해줌.struct NormalStruct {
uint a;
uint b;
uint c;
}
struct MiniMe {
uint32 a;
uint32 b;
uint c;
}
// `mini`는 구조체 압축을 했기 때문에 `normal`보다 가스를 조금 사용함.
NormalStruct normal = NormalStruct(10, 20, 30);
MiniMe mini = MiniMe(10, 20, 30);
struct MiniNotEfficient {
uint32 a;
uint c;
uint32 b;
}
struct MiniMe {
uint32 a;
uint32 b;
uint c;
}
// `NotEfficient`는 uint32 필드가 묶여있지 않아 `Mini`보다 가스를 조금 사용함.
view함수는 가스를 소모하지 않음.view 함수는 외부에서 호출되었을 때 블록체인 상 어떤 것도 수정하지 않기 때문에, 트랜잭션을 만들 지 않음. 따라서 가스 소모도 없음.external view 함수를 쓰는 것이 중요함.동일 컨트랙트 내에 있는
view함수가 아닌, 다른 함수에서 내부적으로 호출될 경우, 가스가 소모됨!
storage는 비싸다.Storage 접근 연산은 비싸다. 특히 write 연산은 더 비쌈.storage 대신, memory를 사용하여, 함수 안에서 호출될 때 마다 배열을 새로구성하고, external view 함수로 외부 호출로 인해 데이터를 볼 수 있는 방법으로 구성하자.storage의 접근 권한이 없음(읽고 쓰기 불가능)message call의 인스턴스가 생성될 때 초기화되는 데이터 영역.Memory는 선형적이며, 바이트 단위 주소 지정이 가능stack machine, 모든 계산 및 수행은 stack 데이터 영역에서 이루어짐.struct와, array의 경우, 명시적으로 저장 공간을 구별해 주어야 할 떄가 있음. 솔리디티는 Statically typed language, 각 변수의 모든 타입 명세되어야함. 솔리디티의 타입은, Value Type과 Reference Type이 존재하며, Reference type은 struct, array, mapping이 존재함.
체크섬 테스트를 통과한 16진수 리터럴, 39 ~ 41 자리의 리터럴은 체크섬 테스트를 통과하지 못하면 에러남. eg) 0xdCad3a6d3569DF655070DEd06cb7A1b2Ccd1D3AF
과학적 표기 기법 지원 : MeE => M * 10**E
가독성을 위한 Underscore 지원 : 123_000 or 0x2eff_abde or 1_2e345_678
Single-quotes('') or double-qoutes("")
여러 연속적 문자열 사용 가능("foo""bar" = "foobar")
bytes1 ~ bytes32와 호환 가능함. 예를 들어, byte32 same = "stringliteral"은, raw byte로 해석되어 할당됨.
Printable ASCII 사용 가능
unicode 키워드는 어떠한 UTF-8 sequence를 포함할 수있음.
keywork hex와 single or double quotes 붙여서 사용
(hex"00122ff", hex'0011_22_ff')
사이 공백이 있는 여러개의 hexadecimal literals은 한개로 합칠 수 있음
(hex"00112233" hex"44556677" is equivalent to hex"0011223344556677")
참조형 타입을 사용할 땐, 명시적으로 데이터 저장 위치를 지정해 주어야 함. 컨트랙트 내부 최외각 부분에서의 변수 위치에서는 data location이 생략 가능함.
memory: Lifetime is limited to an external function call
storage: state variable이 저장되는 곳과 동일, contract의 lifetime과 동일함.
calldata: function arguments를 저장하는 special location, 읽기 전용 공간으로 수정할 수 없음.
copy가 동반됨.memory와 calldata는 0.6.9 이상의 컴파일러 버젼에서 가시성에 상관 없이 사용 가능함.memory to memory 할당은 reference를 생성함.storage to memory 할당은 copy.storage to local storage variable은 referencestorage 할당은 copy.!, &&, ||, ==, != 컴파일 타임에 고정 크기로 할당되거나, 동적으로 사이즈가 변동될 수 있음. 예를 들어, 자연수 k에 대하여 배열 T는 T[k] or T[]로 표현되며 전자는 고정크기, 후자는 동적 크기를 나타냄.
T는 타입을 나타내며, T에는 배열 자체가 될 수 있음. uint[][5]는 5개의 uint 타입이 들어간 고정 크기 배열을 갖는, 동적 크기 배열이란 뜻.
배열 타입은 구조체와 매핑을 포함하여 어떤 유형이던 사용할 수 있음. 단, mapping은 스토리지 데이터 위치에만 저장할 수 있으며, public-visible 함수의 파라미터는 ABI type이어야 함.
x[start:end] : x[start] ~ x[end-1]
start default 0, end default is length of array. both optional.
bytes 와 stringbytes와 string은 배열임. bytes는 bytes1[]과 비슷하나,
bytes1, bytes2... bytes32 : 1 ~ 32byte의 연속적인 데이터
.length bytes : Dynamically-size byte array, value-type이 아님!
string : Dynamically-sized UTF-8 encoded string, value-type이 아님!!
파이썬의 딕셔너리와 같음.
mapping(KeyType KeyName? => ValueType ValueName?);
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.3;
contract Mapping {
mapping(address => uint) public balances;
// Nested mapping
mapping(address => mapping(address => bool)) public isFriend;
function examples() external {
balances[msg.sender] = 123;
uint bal = balances[msg.sender];
uint bal2 = balances[address(1)]; // Not setted, return default vlue
balances[msg.sender] += 456; // 123 + 456 = 579
delete balances[msg.sender]; // Delete balances[msg.sender], reset to default 0
isFriend[msg.sender][address(this)] = true;
}
}
매핑은 storage 저장될 수 있으며 이는 상태 변수만 취급한다는 의미이다. 따라서 함수의 parameter나 return parameter로 사용되지 못한다. 이는 매핑을 포함한 배열이나 구조체에도 적용됨.
KeyName or ValueName은 Optional, 적지 않아도 됨.public 선언이 가능하며, getter funciton의 parameter는 Keytype의 Keyname로, return 값은 ValueType의 ValueName으로 정해진다.address : 20byte 주소 값(이더리움 주소)address payable : address와 동일하지만, transfer send의 멤버가 포함됨.address payable to address 의 경우 Implicit type conversion 가능,address to address payable의 경우 반드시 payable(<address>)를 통하여 명시적(payable())으로 바꿔 주어야 함.balance : 이더리움의 잔고 파악(units of wei) transfer : payable address로의 이더리움 전송, 전송 금액이 부족하거나, 전송할 계정으로부터 거절이 이뤄질 경우 전송 실패.address payable x = payable(0x123);
address myAddress = address(this);
if(x.balance < 10 && myAddress.balance>=10) x.transfer(10); contract definition + import + pragma// SPDX-License-Identifier : MIT
- Version Pragma
- ABI Coder Pragma
- Experimental Pragma
- ABIEncoderV2
- SMTChecker
import "filename";
import * as symbolName from "filename";
import "filename" as symbolName; // Same
import {symbol1 as alias, symbol2} from "filename";
// Rename symbol name (symbol1 -> alias)
// This is a single-line comment.
/*
This is a
multi-line comment.
*/
Storage: 각 계약이 가지고 있는 메모리 공간으로, 256-bits words : 256-bit words의 key-value store임. 저장된 데이터는 블록체인에 올라가기 때문에 영구적임. Low-level에서SSTORE,SLOAD의 opcode를 사용. 비쌈.
Memory: 임시 변수 공간으로(volatile), 실행되는 컨트랙트에 한정된 공간으로, 실행이 끝나면 없어짐. 새로운 메모리 인스턴스는 새로운 message call이 들어오면 얻을 수 있음.
Calldata: read-only, 트랜젝션의 데이터 또는 외부 함수 호출의 매개변수가 저장되는 공간.memory와 비슷하며 byte-addressable 공간임. 정확한 바이트별 접근으로 읽기 연산을 수행할 수 있음.
Stack: 작은 로컬 변수를 저장하는 공간, 읽기 쓰기 요금이 거의 없음. 제한된 공간 - 제한된 크기 영역, 컨트랙트 내부 로컬 변수의 대부분이 이 공간에 저장됨.
Code: 컨트랙트의 소스 코드가 저장되는 공간.

- Solidity 내부 변수가 어디에 위치해 있는가?
- 변수 이름 앞 keyword는 무엇인가?
constant 키워드로 정의된 변수 -> code by defaultstorage by defaultstack by defaultuint256, bytes8, address 등struct 및 array는 키워드로 명시해줘야함(storage or memory or calldata)reference type variable이라 함.array와 같은 자료구조는 데이터가 어디에 저장될 지 명시해줘야함. 아래 3가지 상황에 맞춰, data location은 반드시 명시를 해줘야함.
- 함수 정의에 있어, 받는 parameter (function definition)
- local variables inside function (function body)
- return value는 항상
memory안에 존재.

storage, memory, calldata 모두 함수의 visibility와 상관 없이 사용 가능.
storage는 다른storage및 직접 할당 외,memory또는calldatareference에 저장된 데이터 할당은 불가능함.memory는 키워드로 상관 없이 모두 할당 가능함. 메모리로의 할당은 항상 복사로 수행되기 때문에 원본에 영향을 미치지 않음.calldata는calldatareference로부터면 할당 가능함.

// SPDX-License-Identifier: Apache-2
pragma solidity ^0.8.0;
contract StorageReferences {
bytes someData;
function storageReferences() public {
bytes storage a = someData;
bytes memory b;
bytes calldata c;
// storage variables can reference storage variables as long as the storage reference they refer to is initialized.
bytes storage d = a;
// if the storage reference it refers to was not initiliazed, it will lead to an error
// "This variable (refering to a) is of storage pointer type and can be accessed without prior assignment,
// which would lead to undefined behaviour."
// basically you cannot create a storage reference that points to another storage reference that points to nothing
// f -> e -> (nothing) ???
bytes storage e;
bytes storage f = e;
// storage pointers cannot point to memory pointers (whether the memory pointer was initialized or not
bytes storage x = b;
bytes memory r = new bytes(3);
bytes storage s = r;
// storage pointer cannot point to a calldata pointer (whether the calldata pointer was initialized or not).
bytes storage y = c;
bytes calldata m = msg.data;
bytes storage n = m;
}
}

// SPDX-License-Identifier: Apache-2
pragma solidity ^0.8.0;
contract DataLocationsReferences {
bytes someData;
function memoryReferences() public {
bytes storage a = someData;
bytes memory b;
bytes calldata c;
// this is valid. It will copy from storage to memory
bytes memory d = a;
// this is invalid since the storage pointer x is not initialized and does not point to anywhere.
/// bytes storage x;
/// bytes memory y = x;
// this is valid too. `e` now points to same location in memory than `b`;
// if the variable `b` is edited, so will be `e`, as they point to the same location
// same the other way around. If the variable `e` is edited, so will be `b`
bytes memory e = b;
// this is invalid, as here c is a calldata pointer but is uninitialized, so pointing to nothing.
/// bytes memory f = c;
// a memory reference can point to a calldata reference as long as the calldata reference
// was initialized and is pointing to somewhere in the calldata.
// This simply result in copying the offset in the calldata pointed by the variable reference
// inside the memory
bytes calldata g = msg.data[10:];
bytes memory h = g;
// this is valid. It can copy the whole calldata (or a slice of the calldata) in memory
bytes memory i = msg.data;
bytes memory j = msg.data[4:16];
}
}

// SPDX-License-Identifier: Apache-2
pragma solidity ^0.8.0;
contract DataLocationsReferences {
bytes someData;
function calldataReferences() public {
bytes storage a = someData;
bytes memory b;
bytes calldata c;
// for calldata, the same rule than for storage applies.
// calldata pointers can only reference to the actual calldata or other calldata pointers.
}
}
// 1. memory <- state variable
// : memory 할당은 항상 복사, 할당된 데이터를 변경해도 원본엔 영향이 없음.
contract MemoryCopy {
bytes someData;
constructor() {
someData = bytes("All About Solidity");
}
function copyStorageToMemory() public {
// assigning memory <-- storage
// this load the value from storage and copy in memory
bytes memory value = someData;
// changes are not propagated down in the contract storage
value = bytes("abcd");
}
}
// 2. storage pointer
// : storage pointer가 참초하는 someData, value의 값을 바꾸면 someData의 값도 바뀜.
contract StoragePointer {
uint256[] public someData;
function pushToArrayViaPointer() public {
uint256[] storage value = someData;
value.push(1);
}
}
// 3. memory <- storage pointer
// : 여러 단계를 거쳐도 memory 할당은 복사, someData에 영향을 미치지 않음.
contract MemoryCopy {
uint256[] public someData;
function copyStorageToMemoryViaRef() public {
uint256[] storage storageRef = someData;
uint256[] memory copyFromRef = storageRef;
}
}
인터페이스는 abstract contract와 비슷하지만, 몇가지 제한점이 존재함. 인터페이스는 추상 함수로만 구성되며, 함수의 내용은 상속받는 쪽에서 구현한다.
external이여야 함.modifier를 선언할 수 없음.인터페이스는 말 그대로 다른 contract와 상호 작용을 하기 위한 계층에 불과함. Deploy된 계약의 함수 내용을 전부 다 가져오지 않고, 인터페이스를 선언하여 인터페이스를 통해 해당 계약의 주소를 불러오기만 하면, 그 계약에 정의된 함수를 사용할 수 있음.
예를들어, Counter.sol이라는 배포된 함수가 존재한다고 하고, 내용은 아래와 같다.
// Counter.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract Counter {
uint public count;
function inc() external {
count += 1;
}
function dec() external {
count -= 1;
}
}
위 계약에서 사용된, inc, dec 함수를 다른 MyCounter.sol에서 사용하고 싶다고 해보자. 정의 내용을 전부 가져오는것보다, Counter의 인터페이스인, ICounter를 정의하여 배포된 Counter의 컨트랙트를 가져올 수 있음.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
interface ICounter {
function count() external view returns (uint);
function inc() external;
}
contract MyCounter {
uint public count;
function examples(address _counter) external {
ICounter(_counter).inc();
count = ICounter(_counter).count();
}
}
배포된 Counter.sol의 주소를 넘겨주게 되면, interface ICounter에 선언된 함수의 내용이 Counter.sol에 적힌 내용으로 채워져, 호출 시 Counter.sol의 inc 함수를 사용할 수 있다.
추가로, public으로 정의된 Counter.sol의 count 상태 변수는, getter 함수가 자동으로 생성되기 때문에 count() 함수를 호출하게 되면 Counter.sol의 상태값인 count를 가져올 수 있게 되는 것이다.
인터페이스는 아래와 같이 파일을 나눠 선언할 수도 있음. 인터페이스 자체가 선언된 ICounter.sol은 그 자체로만으로는 배포할 수 없나봄(remix에서 안됨)
// ICounter.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
interface ICounter {
function count() external view returns (uint);
function inc() external;
function dec() external;
}
// Counter.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "./ICounter.sol"
contract Counter is ICounter {
uint public count;
function inc() external {
count += 1;
}
function dec() external {
count -= 1;
}
}
// MyCounter.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "./ICounter.sol";
contract MyCounter {
uint public count;
function examples(address _counter) external {
ICounter(_counter).inc();
}
}
wei = 1gwei = 1e9ether = 1e18secondsminuteshoursdaysweeksfunction f(uint start, uint daysAfter) public {
if(block.timestamp >= start + daysAfter * 1 days) {
// do Something...
}
}
blockhash(uint blockNumber) returns (bytes32)
: 주어진 block의 hash값, blockNumber은 256개의 최근 block 중 한개, 아닐 경우 0 return
block.basefee : 현재 블럭의 베이스 요금
block.chainid | (uint) : 현재 체인 아이디
block.coinbase | (address payable) : 현재 블록 채굴자 주소
block.difficulty | (uint) : 현재 블록의 난이도
block.gaslimit | (uint) : 현재 블록의 숫자
block.prevrandao | (uint) : beacon chain에 의한 난수 ( >= Paris)
block.timestamp | (uint) : 현재 타임스탬프 (seconds since unix epoch)
gasleft() return | (uint256) : remaining gas
msg.data | (bytes calldata) : complete calldata
msg.sender | (address) : message의 sender 주소 (현재 call)
msg.sig | (bytes4) : calldata의 초기 4 byte
msg.value | (uint) : message의 number of wei
tx.gasprice | (uint) : Transaction 가스피
tx.origin | (address) : Transaction의 sender 주소 (full call chain)
tx.origin vs msg.sender ?

타입에 대한 정보를 제공
type(C).name : 컨트랙트의 이름type(C).creationCode : 컨트랙트의 creation bytecodetype(C).runtimeCode : 컨트랙트의 runtime bytecodetype(I).interfaceId : 주어진 인터페이스 EIP-165 식별자, bytes4 valueCode is the law, 배포 이후 컨트랙트는 수정 및 업데이트 불가능.
- public : Compiler가 getter function을 자동으로 생성, 다른 계약에서도 변수 접근 가능. 변수가 선언된 계약 내부의 접근은 storage에서 값을 가져오며, 계약 외부의 접근은 getter를 유발함. Setter는 자동으로 정의되지 않기 때문에 다른 계약이 변수의 값을 바꾸지 못함.
- internal : 변수가 정의된 Contract와 파생된 Contract 내에서만 접근 가능.
- private : 파생된 계약에서는 볼 수 없음.
참고 :
private와internal은 다른 계약에서의 읽기 및 쓰기를 제한하는 것이지만, blockchain 밖의 세상에서는 변수 볼 수 있음.
external : 다른 Contract나 Transaction에서 호출 가능하나,
public :
internal :
private :
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.1 <0.9.0;
contract SimpleAuction {
function bid() public payable { // Function
// ...
}
}
// Helper function defined outside of a contract
function helper(uint x) pure returns (uint) {
return x * 2;
}
contract Ballot {
struct Voter { // struct
uint weight;
bool voted;
address delegate;
uint vote;
}
}
contract Purchase {
enum State { Created, Locked, Inactive }
}
Fixed Array와 Dynamic array로 나뉨. // Array with a fixed length of 2 elements:
uint[2] fixedArray;
// another fixed Array, can contain 5 strings:
string[5] stringArray;
// a dynamic Array - has no fixed size, can keep growing:
uint[] dynamicArray;
function eatHamburgers(string memory _name, uint _amount) public {
}
함수 파라미터 이름 앞에
_를 추가하여 전역 변수와의 차별성을 두는 관행이 있음.
string greeting = "Hey! what's up?";
function sayHello() public returns (string memory) {
return greeting;
}
uint8 a = 5;
uint b = 6;
// throws an error because a * b returns a uint, not uint8:
uint8 c = a * b;
// we have to typecast b as a uint8 to make it work:
uint8 c = a * uint8(b);
private: 컨트랙트 내부 다른 함수들에서만 호출 가능.internal: 컨트랙트를 상속하는 컨트랙트에서도 호출될 수 있음.external: "오직" 컨트랙트 외부에서만 호출 가능public: 내 외부 어디에서든 호출 가능함.
public by default.public화 시키지 않아야 함. _를 사용하는 관행이 있음.
view: 해당 함수가 실행되어도 어떠한 데이터의 저장 및 변경이 없음을 알려줌pure: 해당 함수가 데이터의 수정 + 읽기까지 수행하지 않음을 알려줌.
// View function example
function sayHello() public view returns(string memory) {
}
// Pure function example
function _multiply(uint a, uint b) public pure returns(uint) {
return a * b;
}
사용자가 만든 커스텀 제어자, OpenZeppelin Ownable Contract가 대표적임. (onlyOwner)
사용자 정의 modifier는 인수를 갖을 수 있음.
함수의 반복되는 확인 로직 등을 modifier로 직접 만들어 모듈화 시킬 수 있음.
mapping (uint => uint) public age;
modifier olderThan(uint _age, uint _userId) {
require (age[_userId] >= _age);
_; // require 통과 이후, 함수의 나머지 내용 실행 (필수)
}
// 선언한 Modifier에 의해 require() age 검수 시행.
// 함수 인자로 받는 _userId를 modifier 인자로 넘겨줄 수 있음.
function driveCar(uint _userId) public olderThan(16, _userId) {
// do something...
}
payable modifier를 추가하여 함수 실행에 컨트랙트에 비용이 지불되도록 할 수 있음.payable 지시자가 없을 때 비용을 지불하려 하면, 함수가 트랜젝션을 거부함.msg.value로 트랜젝션으로 들어온 금액을 확인할 수 있음.Ownable 사용시, owner.transfer 함수를 사용하여 컨트랙트에 저장된 이더를 인출할 수 있음.contract GetPaid is Ownerble {
function withdraw() external onlyOwner {
owner.transfer(this.balance);
}
}
indexed)을 추가할 수 있음. 이를 통해 로그의 데이터 부분이 아닌, 토픽 구조에 매개변수가 표시됨. 매개변수에 인덱싱 속성이 없는 경우, 해당 매개변수는 데이터 부분에 ABI 로 인코딩됨.indexed 키워드가 붙은 이벤트 로깅 데이터는, 특정 데이터로 이벤트 쿼리를 할 수 있음. 특정 이벤트의 특정 주소만 가지고 오는 등의 필터링이 가능해짐.예를 들어, IUniswapV2Pair interface에는 아래와 같은 event가 있음
event Approval(address indexed owner, address indexed spender, uint value);
이로 인해, 출력되는 로그는, 아래와 같음.indexed키워드가 사용된 address spender, address owner는 Topics에, value는 Data에 로깅되는 것을 볼 수 있음.
Event는 Contract의 상속 가능한 멤버로, 이벤트가 발생하면 트랜잭션 로그에 전달된 인수를 블록체인에 저장하며, 컨트랙트 주소를 사용하여 액세스 할 수 있음. 생성된 이벤트는 컨트랙트 내에서 접근할 수 없으며, 이벤트를 생성하고 발신한 컨트랙트에서도 접근할 수 없음.// Declare Event
event Deposit(address indexed _from, byte32 indexed _id, uint _value);
// Emit an event
emit Deposit(msg.sender, _id, msg.value);
event IntegerAdded(uint x, uint y, uint result);
function add(uint _x, uint _y) public {
uint result = _x + _y;
// IntegerAdded Event가 실행됨.
IntegerAdded(_x, _y, result);
return result;
}
위 이벤트가 수행되면, 앱단에서의 접근은,
contractName.IntegersAdded(function(error, result) {
// Actions...
})
single parameter of bytes returns a random 256-bit(32bytes) hexadecimal number.now의 타임스탬프 값, msg.sender, nonce(함수 실행에 딱 한번만 사용되는 숫자, 사용 이후 업데이트됨)// Generate a random number between 1 and 100
uint randNonce = 0;
uint random = uint(keccak256(now, msg.sender, randNonce)) % 100;
//abi.encodePacked -> string to bypes packing
keccak256(abi.encodePacked("aaab"));
둘 다 인코딩의 기능을 담당하지만, encodePacked는 압축함. encodePacked는 두 개의 dynamic input이 들어갈 경우, 경우에 따라 동일한 output을 도출하는 경우가 종종 있음.
예를 들어, "aaa", "bbb"과, "aa", "abbb"라는 2개의 입력을 encodePacked로 연산 할 경우, 동일한 결과값을 도출함.
따라서, 이를 keccak256의 입력으로 사용할 경우, 입력값이 달라졌지만 출력값이 동일하여, hash collision을 초래할 수 있음.
이를 해결하는 경우는, 사이에 정적인 데이터 입력값을 추가하거나, abi.encode()를 사용하여야 함.
ERC20 or ERC721 토큰이 대표적임.ERC721 토큰은, nonfungible, 각 토큰이 분할이 불가능하며, 고유의 identifier를 가지고 있음. 표준에 의해, 경매나 중계 로직을 직접 구현하지 않아도 ERC721 자산을 거래할 수 있는 거래소에서 공통 로직을 사용할 수 있음.contract ERC721 {
event Transfer(address indexed _from, address indexed _to, uint256 _tokenId);
event Approval(address indexed _owner, address indexed _approved, uint256 _tokenId);
function balanceOf(address _owner) public view returns (uint256 _balance);
function ownerOf(uint256 _tokenId) public view returns (address _owner);
function transfer(address _to, uint256 _tokenId) public;
function approve(address _to, uint256 _tokenId) public;
function takeOwnership(uint256 _tokenId) public;
}
컨트랙트에 대한 권한을 선정할 수 있음.
Ownable특정 컨트랙트에 대한 소유권자를 선정하고, 함수에 대하여 실행.
modifier onlyOwner : 소유자만 실행할 수 있는 권한.transferOwnership(address newOwner) : 새로운 Owner에 대하여 소유권 이전renounceOwnership() : address(0)으로 소유권 이전, onlyOwner의 함수는 실행할 수 없음./**
* @dev Throws if called by any account other than the owner.
*/
modifier onlyOwner() {
require(msg.sender == owner);
_;
}
Ownable2StepOwnable 상속, 기존 Ownable 함수 사용 가능하지만, 소유권 이전에 대한 방식이 변경됨. 단지 한 단계의 함수 실행으로 소유권이 변경되었던 Ownable과 달리, Ownable2Step은 소유권 이전 요청 -> 소유권 이전 허가로 2단계의 소유권 이전 단계를 나눔. accept되지 않은 소유권 이전은, 실제로 반영되지 않기 때문에 소유권 이전에서 발생한 실수를 미연에 방지할 수 있음.
_pendingOwner : 소유권을 받을 예정의 임시 OwnerpendingOwner() : get pendingOwner.transferOwnership : 소유권 이전 작업의 시작, event로 명시함.acceptOwnership : 소유권을 이전 받는 사람이 호출, 소유권 이전을 확정함.권한에 대한 이름을 명명하여 역할-기반의 access control 메커니즘 사용 가능.
public constant로 선언, bytes32 identifier로 선언 시 keccak256 자주 사용.grantRole(role, account) : 계정에 역할 부여revokeROle(role, account) : 계정이 역할 박탈배포되는 컨트랙트 앞에, proxy, proxyAdmin 컨트랙트를 두어서, l
오버플로우, 언더플로우가 방지된 Math library
using SafeMath for uint256; , then call functions below.library 키워드는, 사용하는 contract 내부에서 using 키워드를 통해 라이브러리를 사용할 수 있게 해줌.library SafeMath {
function mul(uint256 a, uint256 b) internal pure returns (uint256) {
if (a == 0) {
return 0;
}
uint256 c = a * b;
assert(c / a == b);
return c;
}
function div(uint256 a, uint256 b) internal pure returns (uint256) {
// assert(b > 0); // Solidity automatically throws when dividing by 0
uint256 c = a / b;
// assert(a == b * c + a % b); // There is no case in which this doesn't hold
return c;
}
function sub(uint256 a, uint256 b) internal pure returns (uint256) {
assert(b <= a);
return a - b;
}
function add(uint256 a, uint256 b) internal pure returns (uint256) {
uint256 c = a + b;
assert(c >= a);
return c;
}
}
contract foo {
using SafeMath for uint;
uint test = 2;
test = test.mul(3); // Use SafeMath methods
}
call은, 컨트랙트의 view와 pure 함수를 사용, 로컬 노드에서 트랜잭션 발생하지 않기 때문에 트랜잭션 서명 및 가스비 지불할 필요 없음.
send는 view와 pure를 제외한 함수에 대하여 사용. 가스를 지불하여 트랜잭션을 만들고, 서명이 이루어지면 함수 호출이 이루어짐.
public 변수는 자동으로 getter 함수가 생성됨. 따라서 public 변수는 함수처럼 인자를 입력하여 호출할 수 있음.
require, revert, assert, 가스피는 환불되며, 변경된 상태 변화는 다시 되돌려진다.assert는 Nested 조건문 안에서 사용이 많이 됨.// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract Error {
// require
function testRequire(uint _i) public pure {
require(_i <= 10, "i > 10");
// pass do something
}
// revert
function testRevert(uint _i) public pure {
if(_i > 10) {
if(_i > 2) {
revert("i > 10"); // Nested
}
}
}
// assert
uint public num = 123;
function testAssert() public view {
assert(num == 123);
}
// Custom Error
error MyError(address caller, uint i); //only Revert
function testCustomError(uint _i) public view {
if(_i > 10) {
revert MyError(msg.sender, _i);
}
}
}
Inline Assembly 코드를 작성하기 위해 사용하는 언어, assembly{ ... } 코드블럭 안에서 사용하는 코드이다.
assembly 코드블럭은 별도의 namespace를 가지지 않기에, 다른 코드 블럭에서 정의된 변수를 호출한다던지, 함수를 호출하는 것은 불가능하다.작성한 solidity 컨트랙트를 ethers.js나 기타 방법으로 만드는 것 외에, solidity 코드로 컨트랙트를 생성하는 방법도 존재한다.
new 키워드로 만들기Contract 내부에서 new 키워드로 다른 컨트랙트를 생성할 수 있다.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract D {
uint public x;
constructor(uint a) payable {
x = a;
}
}
contract C {
D d = new D(4); // will be executed as part of C's constructor, 계약 C가 생성되면 계약 D도 생성됨.
function createD(uint arg) public {
D newD = new D(arg);
newD.x();
}
function createAndEndowD(uint arg, uint amount) public payable {
// Send ether along with the creation
D newD = new D{value: amount}(arg);
newD.x();
}
}
create2byte32 값인 option salt를 사용하면 salt 값과, 만들어 지는 계약의 bytecode와, 제작자 contract의 주소를 사용하여 생성될 계약의 주소를 만든다.
이 방식은, 계약을 생성하기 이전에 계약이 생성 될 주소를 미리 계산해 볼 수 있다.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract D {
uint public x;
constructor(uint a) {
x = a;
}
}
contract C {
function createDSalted(bytes32 salt, uint arg) public {
// This complicated expression just tells you how the address
// can be pre-computed. It is just there for illustration.
// You actually only need ``new D{salt: salt}(arg)``.
address predictedAddress = address(uint160(uint(keccak256(abi.encodePacked(
bytes1(0xff),
address(this),
salt,
keccak256(abi.encodePacked(
type(D).creationCode,
abi.encode(arg)
))
)))));
D d = new D{salt: salt}(arg);
require(address(d) == predictedAddress);
}
}
fallback과 receive, 컨트랙트 콜에서 실행시킨 함수가 존재하지 않을 때 실행되는 함수. 보통 컨트랙트가 직접적으로 ETH를 전송받을 때 사용한다. 직접 컨트랙트에 이더를 전송하게 되면 fallback 함수가 실행되는 방식.(payable로 선언해야 함.)
fallback : 이더가 직접 전송되었을 때 msg.data가 존재하지 않거나, 존재하더라도 receive 함수가 정의되지 않았을 때 실행됨receive : 이더가 직접 전송되었을 때 msg.data가 존재하고, 선언되었을 떄 실행됨.transfer : 2300 gas를 가지고 주소에 전송, 실패시 revertssend : 2300 gas를 가지고 주소에 전송, 실패시 returns boolcall : 모든 가스를 가지고 returns bool and data// SPDX-License-Identifier: MIT
pragma solidity ^0.8.3;
contract SendETH {
constructor() payable {} // deployed payable
fallback() external payable {}
function sendViaTransfer(address payable _to) external payable {
_to.transfer(123);
}
// 잘 사용하지 않음.
function sendViaSend(address payable _to) external payable {
bool sent = _to.send(123);
require(sent, "send failed");
}
function sendViaCall(address payable _to) external payable {
(bool success, ) = _to.call{value: 123}("");
require(success, "call failed");
}
}
contract EthReceiver {
event Log(uint amount, uint gas);
receive() external payable {
emit Log(msg.value, gasleft());
}
}
selfdestruct : 컨트랙트 삭제, Ether를 어느 주소로 보낼 수 있음. 받는 주소가 컨트랙트이며, fallback 함수가 없다고 해도 이더를 받을 수 있음.
아래 Helper 컨트랙트는 fallback 함수가 없기 때문에 직접적인 이더리움 전송을 받을 수 없음. 하지만, selfdestruct에 의한 이더 전송은 가능함.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.3;
contract Kill {
constructor() payable {
}
function kill() external {
selfdestruct(payable(msg.sender));
}
function testCall() external pure returns (uint) {
return 123;
}
}
contract Helper {
function getBalance() external view returns (uint) {
return address(this).balance;
}
function kill(Kill _kill) external {
_kill.kill();
}
}
유저 foo와, 스마트 컨트랙트 A, B가 있다고 하자.
A가, B의 함수를 call한다면, B의 입장에서는 msg.sender = address(A)가 될 것이다. 만약 B에서 호출된 함수가 상태 변수를 변경하게 된다면, B의 상태 변수가 변경될 것이다.
A가 B의 함수를 delegateCall하게 된다면, A는 B의 함수를, 마치 자신의 것 마냥 사용하게 된다. B의 함수를 가져와 실행하지만, 자신의 함수인것마냥 실행하기 때문에 B의 호출에서 msg.sender = address(foo)가 된다. 또한, 이 함수에서 변경되는 상태 변수는 A의 상태변수가 된다.
Proxy 역할을 하는 스마트 컨트랙트가, 실질 로직만 담겨있는 스마트 컨트랙트를 delegateCall하게 되면, 외부에 노출된 Proxy와의 통신은 그대로 유지되며, 만약 로직이 업데이트 된다면, 로직이 담긴 스마트 컨트랙트의 배포와, Proxy 내부에 포인트 주소만 변경하게되면 된다.
SimpleToken, SmartBank, Player... /// or /** ... */transfer 함수의 direct call은 잠재적인 보안 위험으로 인해 권장하지 않음.컨트랙트 내부에서 사용자의 역할을 정의, 역할에 따라 권한을 부여할 수 있음.