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
와 string
bytes
와 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
또는calldata
reference에 저장된 데이터 할당은 불가능함.memory
는 키워드로 상관 없이 모두 할당 가능함. 메모리로의 할당은 항상 복사로 수행되기 때문에 원본에 영향을 미치지 않음.calldata
는calldata
reference로부터면 할당 가능함.
// 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
= 1e18seconds
minutes
hours
days
weeks
function 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);
_;
}
Ownable2Step
Ownable
상속, 기존 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();
}
}
create2
byte32 값인 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은 잠재적인 보안 위험으로 인해 권장하지 않음.컨트랙트 내부에서 사용자의 역할을 정의, 역할에 따라 권한을 부여할 수 있음.