컨트랙트
컨트랙트 (Contracts)
Solidity의 컨트랙트는 객체지향 언어의 클래스와 비슷해요. 컨트랙트는 상태 변수에 영속 데이터를 담고, 이 변수들을 수정할 수 있는 함수를 가져요. 다른 컨트랙트(인스턴스)에서 함수를 호출하면 EVM 함수 호출이 수행되어 컨텍스트가 전환돼서, 호출하는 컨트랙트의 상태 변수에 접근할 수 없게 돼요. 어떤 일이 일어나려면 컨트랙트와 그 함수가 호출되어야 해요. 특정 이벤트에서 함수를 자동으로 호출하는 "cron" 개념은 이더리움에 없어요.
출처: 문서
본문
컨트랙트 만들기 (Creating Contracts)
컨트랙트는 이더리움 트랜잭션을 통해 "외부에서" 또는 Solidity 컨트랙트 안에서 만들어질 수 있어요. Remix 같은 IDE는 UI 요소를 사용해 생성 과정을 원활하게 만들어요. 이더리움에서 프로그램적으로 컨트랙트를 만드는 한 가지 방법은 JavaScript API web3.js를 통한 것이에요. web3.eth.Contract라는 함수가 있어서 컨트랙트 생성을 용이하게 해요.
컨트랙트가 생성될 때, 그 생성자(constructor 키워드로 선언된 함수)가 한 번 실행돼요. 생성자는 선택 사항이에요. 생성자는 하나만 허용되므로 오버로딩은 지원되지 않아요. 생성자가 실행된 후 컨트랙트의 최종 코드가 블록체인에 저장돼요. 이 코드는 모든 public과 external 함수와, 그곳에서 함수 호출을 통해 도달할 수 있는 모든 함수를 포함해요. 배포된 코드에는 생성자 코드나 생성자에서만 호출되는 내부 함수는 포함되지 않아요. 내부적으로 생성자 인자는 컨트랙트 코드 자체 뒤에 ABI 인코딩되어 전달되지만, web3.js를 사용한다면 신경 쓸 필요가 없어요.
컨트랙트가 다른 컨트랙트를 만들려면, 생성되는 컨트랙트의 소스 코드(와 바이너리)를 생성자가 알고 있어야 해요. 즉 순환 생성 의존성은 불가능해요.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.22 <0.9.0;
contract OwnedToken {
// `TokenCreator` is a contract type that is defined below.
// It is fine to reference it as long as it is not used
// to create a new contract.
TokenCreator creator;
address owner;
bytes32 name;
// This is the constructor which registers the
// creator and the assigned name.
constructor(bytes32 name_) {
// State variables are accessed via their name
// and not via e.g. `this.owner`. Functions can
// be accessed directly or through `this.f`,
// but the latter provides an external view
// to the function. Especially in the constructor,
// you should not access functions externally,
// because the function does not exist yet.
// See the next section for details.
owner = msg.sender;
// We perform an explicit type conversion from `address`
// to `TokenCreator` and assume that the type of
// the calling contract is `TokenCreator`, there is
// no real way to verify that.
// This does not create a new contract.
creator = TokenCreator(msg.sender);
name = name_;
}
function changeName(bytes32 newName) public {
// Only the creator can alter the name.
// We compare the contract based on its
// address which can be retrieved by
// explicit conversion to address.
if (msg.sender == address(creator))
name = newName;
}
function transfer(address newOwner) public {
// Only the current owner can transfer the token.
if (msg.sender != owner) return;
// We ask the creator contract if the transfer
// should proceed by using a function of the
// `TokenCreator` contract defined below. If
// the call fails (e.g. due to out-of-gas),
// the execution also fails here.
if (creator.isTokenTransferOK(owner, newOwner))
owner = newOwner;
}
}
contract TokenCreator {
function createToken(bytes32 name)
public
returns (OwnedToken tokenAddress)
{
// Create a new `Token` contract and return its address.
// From the JavaScript side, the return type
// of this function is `address`, as this is
// the closest type available in the ABI.
return new OwnedToken(name);
}
function changeName(OwnedToken tokenAddress, bytes32 name) public {
// Again, the external type of `tokenAddress` is
// simply `address`.
tokenAddress.changeName(name);
}
// Perform checks to determine if transferring a token to the
// `OwnedToken` contract should proceed
function isTokenTransferOK(address currentOwner, address newOwner)
public
pure
returns (bool ok)
{
// Check an arbitrary condition to see if transfer should proceed
return keccak256(abi.encodePacked(currentOwner, newOwner))[0] == 0x7f;
}
}
가시성과 Getter (Visibility and Getters)
상태 변수 가시성 (State Variable Visibility)
public
public 상태 변수는 컴파일러가 자동으로 getter 함수를 생성해 다른 컨트랙트가 그 값을 읽을 수 있게 해준다는 점에서만 internal과 다르다. 같은 컨트랙트 안에서 사용될 때 외부 접근(예: this.x)은 getter를 호출하는 반면, 내부 접근(예: x)은 스토리지에서 변수 값을 직접 가져와. setter 함수는 생성되지 않으므로 다른 컨트랙트가 그 값을 직접 수정할 수 없어.
internal internal 상태 변수는 정의된 컨트랙트 안과 파생 컨트랙트 안에서만 접근될 수 있어. 외부에서 접근할 수 없어. 이것이 상태 변수의 기본 가시성 수준이야.
private private 상태 변수는 internal과 같지만 파생 컨트랙트에서 보이지 않아.
경고: 무언가를 private이나 internal로 만드는 것은 다른 컨트랙트가 정보를 읽거나 수정하지 못하게만 막을 뿐, 블록체인 외부의 전 세계에는 여전히 보여.
함수 가시성 (Function Visibility)
Solidity는 두 종류의 함수 호출을 알아: 실제 EVM 메시지 호출을 만드는 외부(external) 호출과 그렇지 않은 내부(internal) 호출. 게다가 내부 함수는 파생 컨트랙트에서 접근할 수 없게 만들 수 있어. 이것이 함수의 네 가지 가시성 타입을 낳아.
external
external 함수는 컨트랙트 인터페이스의 일부이므로 다른 컨트랙트와 트랜잭션을 통해 호출될 수 있어. external 함수 f는 내부적으로 호출될 수 없어(즉 f()는 작동하지 않지만 this.f()는 작동).
public public 함수는 컨트랙트 인터페이스의 일부이며 내부적으로나 메시지 호출을 통해 호출될 수 있어.
internal internal 함수는 현재 컨트랙트 안이나 그것에서 파생된 컨트랙트 안에서만 접근될 수 있어. 외부에서 접근할 수 없어. 컨트랙트의 ABI를 통해 외부에 노출되지 않으므로 매핑이나 storage 참조 같은 내부 타입의 파라미터를 받을 수 있어.
private private 함수는 internal과 같지만 파생 컨트랙트에서 보이지 않아.
경고: 무언가를 private이나 internal로 만드는 것은 다른 컨트랙트가 정보를 읽거나 수정하지 못하게만 막을 뿐, 블록체인 외부의 전 세계에는 여전히 보여.
가시성 지정자는 상태 변수에서는 타입 뒤에, 함수에서는 파라미터 목록과 반환 파라미터 목록 사이에 주어진다.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;
contract C {
function f(uint a) private pure returns (uint b) { return a + 1; }
function setData(uint a) internal { data = a; }
uint public data;
}
다음 예에서 D는 c.getData()를 호출해 상태 스토리지의 data 값을 가져올 수 있지만 f는 호출할 수 없어. 컨트랙트 E는 C에서 파생되므로 compute를 호출할 수 있어.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;
contract C {
uint private data;
function f(uint a) private pure returns(uint b) { return a + 1; }
function setData(uint a) public { data = a; }
function getData() public view returns(uint) { return data; }
function compute(uint a, uint b) internal pure returns (uint) { return a + b; }
}
// This will not compile
contract D {
function readData() public {
C c = new C();
uint local = c.f(7); // error: member `f` is not visible
c.setData(3);
local = c.getData();
local = c.compute(3, 5); // error: member `compute` is not visible
}
}
contract E is C {
function g() public {
C c = new C();
uint val = compute(3, 5); // access to internal member (from derived to parent contract)
}
}
Getter 함수 (Getter Functions)
컴파일러는 모든 public 상태 변수에 대해 getter 함수를 자동으로 생성해. 아래 주어진 컨트랙트에 대해 컴파일러는 인자를 받지 않고 상태 변수 data의 값인 uint를 반환하는 data라는 함수를 생성할 거야. 상태 변수는 선언될 때 초기화될 수 있어.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;
contract C {
uint public data = 42;
}
contract Caller {
C c = new C();
function f() public view returns (uint) {
return c.data();
}
}
getter 함수는 external 가시성을 가져. 기호가 내부적으로 접근되면(this. 없이) 상태 변수로 평가돼. 외부적으로 접근되면(this.와 함께) 함수로 평가돼.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;
contract C {
uint public data;
function x() public returns (uint) {
data = 3; // internal access
return this.data(); // external access
}
}
배열 타입의 public 상태 변수가 있으면 생성된 getter 함수를 통해 배열의 단일 요소만 가져올 수 있어. 이 메커니즘은 전체 배열을 반환할 때 높은 가스 비용을 피하기 위해 존재해. 어떤 개별 요소를 반환할지 지정하는 데 인자를 사용할 수 있어. 예를 들어 myArray(0)처럼. 한 번의 호출로 전체 배열을 반환하려면 함수를 작성해야 해. 예를 들어:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;
contract arrayExample {
// public state variable
uint[] public myArray;
// Getter function generated by the compiler
/*
function myArray(uint i) public view returns (uint) {
return myArray[i];
}
*/
// function that returns entire array
function getArray() public view returns (uint[] memory) {
return myArray;
}
}
이제 단일 요소를 반환하는 myArray(i) 대신 getArray()를 사용해 전체 배열을 가져올 수 있어. 다음 예는 더 복잡해:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;
contract Complex {
struct Data {
uint a;
bytes3 b;
mapping(uint => uint) map;
uint[3] c;
uint[] d;
bytes e;
}
mapping(uint => mapping(bool => Data[])) public data;
}
다음 형태의 함수를 생성해. 구조체의 매핑과 배열(바이트 배열 제외)은 개별 구조체 멤버를 선택하거나 매핑에 키를 제공할 좋은 방법이 없으므로 생략돼:
function data(uint arg1, bool arg2, uint arg3)
public
returns (uint a, bytes3 b, bytes memory e)
{
a = data[arg1][arg2][arg3].a;
b = data[arg1][arg2][arg3].b;
e = data[arg1][arg2][arg3].e;
}
함수 수정자 (Function Modifiers)
수정자는 선언적인 방식으로 함수의 동작을 바꾸는 데 사용될 수 있어. 예를 들어 함수를 실행하기 전에 조건을 자동으로 검사하는 데 수정자를 사용할 수 있어. 수정자는 컨트랙트의 상속 가능한 속성이며 파생 컨트랙트가 재정의할 수 있지만, virtual로 표시된 경우에만 가능해. 자세한 내용은 수정자 재정의를 보세요.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.1 <0.9.0;
contract owned {
constructor() { owner = payable(msg.sender); }
address payable owner;
// This contract only defines a modifier but does not use
// it: it will be used in derived contracts.
// The function body is inserted where the special symbol
// `_;` in the definition of a modifier appears.
// This means that if the owner calls this function, the
// function is executed and otherwise, an exception is
// thrown.
modifier onlyOwner {
require(
msg.sender == owner,
"Only owner can call this function."
);
_;
}
}
contract priced {
// Modifiers can receive arguments:
modifier costs(uint price) {
if (msg.value >= price) {
_;
}
}
}
contract Register is priced, owned {
mapping(address => bool) registeredAddresses;
uint price;
constructor(uint initialPrice) { price = initialPrice; }
// It is important to also provide the
// `payable` keyword here, otherwise the function will
// automatically reject all Ether sent to it.
function register() public payable costs(price) {
registeredAddresses[msg.sender] = true;
}
// This contract inherits the `onlyOwner` modifier from
// the `owned` contract. As a result, calls to `changePrice` will
// only take effect if they are made by the stored owner.
function changePrice(uint price_) public onlyOwner {
price = price_;
}
}
contract Mutex {
bool locked;
modifier noReentrancy() {
require(
!locked,
"Reentrant call."
);
locked = true;
_;
locked = false;
}
/// This function is protected by a mutex, which means that
/// reentrant calls from within `msg.sender.call` cannot call `f` again.
/// The `return 7` statement assigns 7 to the return value but still
/// executes the statement `locked = false` in the modifier.
function f() public noReentrancy returns (uint) {
(bool success,) = msg.sender.call("");
require(success);
return 7;
}
}
컨트랙트 C에 정의된 수정자 m에 접근하려면 C.m을 사용해 가상 조회 없이 참조할 수 있어. 현재 컨트랙트나 그 기본 컨트랙트에 정의된 수정자만 사용할 수 있어. 수정자는 라이브러리에도 정의될 수 있지만, 그 사용은 같은 라이브러리의 함수로 제한돼. 여러 수정자는 공백으로 구분된 목록으로 지정해 함수에 적용되며, 제시된 순서로 평가돼.
수정자는 수정하는 함수의 인자와 반환 값에 암시적으로 접근하거나 변경할 수 없어. 그 값은 호출 지점에서 명시적으로만 수정자에 전달될 수 있어. 함수 수정자에서 수정자가 적용된 함수를 언제 실행할지 지정하는 것이 필요해. 자리 표시자 문장(단일 밑줄 문자 _로 표시)은 수정되는 함수의 본문이 삽입되어야 할 위치를 나타내는 데 사용돼. 자리 표시자 연산자가 변수 이름의 앞/뒤 밑줄을 사용하는 것과 다르다는 점을 참고하세요. 이는 스타일 선택일 뿐이에요.
수정자나 함수 본문에서의 명시적 return은 현재 수정자나 함수 본문만 떠나. 반환 변수는 할당되고 제어 흐름은 앞선 수정자의 _ 뒤에서 계속돼.
경고: Solidity의 초기 버전에서는 수정자를 가진 함수의 return 문장이 다르게 동작했어.
return;을 가진 수정자에서의 명시적 return은 함수가 반환하는 값에 영향을 주지 않아. 그러나 수정자는 함수 본문을 전혀 실행하지 않기로 선택할 수 있고, 그 경우 반환 변수는 함수가 빈 본문을 가진 것처럼 기본값으로 설정돼.
_ 기호는 수정자에 여러 번 나타날 수 있어. 각 발생은 함수 본문으로 대체되고, 함수는 마지막 발생의 반환 값을 반환해. 수정자 인자에는 임의의 표현식이 허용되고, 이 컨텍스트에서 함수에서 보이는 모든 기호는 수정자에서도 보여. 수정자에서 도입된 기호는 (오버라이딩으로 바뀔 수 있으므로) 함수에서 보이지 않아.
트랜지언트 스토리지 (Transient Storage)
트랜지언트 스토리지는 memory, storage, calldata(그리고 return-data와 code) 외의 또 다른 데이터 위치이며, EIP-1153이 해당 opcode TSTORE와 TLOAD와 함께 도입했어. 이 새로운 데이터 위치는 키-값 저장소처럼 동작하며, storage와 유사해. 주요 차이는 트랜지언트 스토리지의 데이터가 영구적이지 않고 현재 트랜잭션으로만 범위가 제한되며, 그 이후 0으로 리셋된다는 것. 트랜지언트 스토리지의 내용은 수명과 크기가 매우 제한적이므로 상태의 일부로 영구 저장될 필요가 없고, 관련 가스 비용이 storage보다 훨씬 낮아.
트랜지언트 스토리지를 사용하려면 EVM 버전 cancun 이상이 필요해. 트랜지언트 스토리지 변수는 제자리에서 초기화될 수 없어. 즉 선언 시 할당될 수 없는데, 생성 트랜잭션이 끝나면 값이 지워져 초기화가 무효화되기 때문이야. 트랜지언트 변수는 밑바탕 타입에 따라 기본값으로 초기화돼. constant와 immutable 변수는 값이 인라인되거나 코드에 직접 저장되므로 트랜지언트 스토리지와 충돌해. 트랜지언트 스토리지 변수는 storage와 완전히 독립적인 주소 공간을 가지므로, 트랜지언트 상태 변수의 순서가 storage 상태 변수의 배치에 영향을 주지 않고 그 반대도 마찬가지야. 모든 상태 변수가 같은 네임스페이스를 공유하므로 구별되는 이름이 필요해.
트랜지언트 스토리지의 값이 영속 스토리지의 값과 같은 방식으로 패킹된다는 것도 중요해. 스토리지 배치에 대한 자세한 내용은 스토리지 배치를 보세요. 그 외에도 트랜지언트 변수는 가시성을 가질 수 있고, public 변수는 평소처럼 getter 함수가 자동으로 생성돼. 현재 그러한 transient 데이터 위치 사용은 값 타입 상태 변수 선언에만 허용된다는 점을 참고해. 배열, 매핑, 구조체 같은 참조 타입과 지역 변수나 파라미터 변수는 아직 지원되지 않아.
트랜지언트 스토리지의 예상되는 표준 사용 사례는 더 저렴한 재진입 잠금인데, 다음에 보여주듯 opcode로 쉽게 구현할 수 있어.
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.28;
contract Generosity {
mapping(address => bool) sentGifts;
bool transient locked;
modifier nonReentrant {
require(!locked, "Reentrancy attempt");
locked = true;
_;
// Unlocks the guard, making the pattern composable.
// After the function exits, it can be called again, even in the same transaction.
locked = false;
}
function claimGift() nonReentrant public {
require(address(this).balance >= 1 ether);
require(!sentGifts[msg.sender]);
(bool success, ) = msg.sender.call{value: 1 ether}("");
require(success);
// In a reentrant function, doing this last would open up the vulnerability
sentGifts[msg.sender] = true;
}
}
트랜지언트 스토리지는 영속 스토리지와 같은 방식으로 소유한 컨트랙트에 private이야. 소유 컨트랙트 프레임만 자신의 트랜지언트 스토리지에 접근할 수 있고, 접근할 때 모든 프레임이 같은 트랜지언트 저장소에 접근해. 트랜지언트 스토리지는 EVM 상태의 일부이며 영속 스토리지와 같은 가변성 강제를 받아. 그러므로 그것에 대한 어떤 읽기 접근도 pure가 아니고 쓰기 접근도 view가 아니야. TSTORE opcode가 STATICCALL의 컨텍스트에서 호출되면 수정을 수행하는 대신 예외가 발생해. TLOAD는 STATICCALL의 컨텍스트에서 허용돼.
트랜지언트 스토리지가 DELEGATECALL이나 CALLCODE의 컨텍스트에서 사용되면, 트랜지언트 스토리지의 소유 컨트랙트는 영속 스토리지와 마찬가지로 DELEGATECALL이나 CALLCODE 명령을 발행한 컨트랙트(호출자)야. 트랜지언트 스토리지가 CALL이나 STATICCALL의 컨텍스트에서 사용되면, 트랜지언트 스토리지의 소유 컨트랙트는 CALL이나 STATICCALL 명령의 대상인 컨트랙트(피호출자)야.
참고:
DELEGATECALL의 경우 트랜지언트 스토리지 변수에 대한 참조가 현재 지원되지 않으므로, 라이브러리 호출에 전달하는 것은 불가능해. 라이브러리에서 트랜지언트 스토리지에 대한 접근은 인라인 어셈블리로만 가능해. 프레임이 되돌아가면 프레임 진입과 반환 사이에 발생한 트랜지언트 스토리지에 대한 모든 쓰기가 되돌아가며, 내부 호출에서 발생한 것도 포함돼. 외부 호출의 호출자는 try ... catch 블록을 사용해 내부 호출에서 되돌림이 위로 전파되는 것을 막을 수 있어.
스마트 컨트랙트의 합성 가능성과 트랜지언트 스토리지의 주의사항 (Composability of Smart Contracts and the Caveats of Transient Storage)
EIP-1153의 명세에서 언급된 주의사항을 고려할 때, 스마트 컨트랙트의 합성 가능성을 보존하기 위해 트랜지언트 스토리지의 더 고급 사용 사례에 최대한 주의를 기울이는 것이 권장돼. 스마트 컨트랙트에서 합성 가능성은 개별 스마트 컨트랙트에 대한 여러 호출을 더 복잡한 애플리케이션으로 구성할 수 있도록, 자급자족적 동작을 달성하기 위한 매우 중요한 설계 원칙이야. 지금까지 EVM은 복잡한 트랜잭션 안의 스마트 컨트랙트에 대한 여러 호출이 여러 트랜잭션에 걸쳐 펼쳐진 컨트랙트에 대한 여러 호출과 실질적으로 구별할 수 없기 때문에 합성 가능한 동작을 크게 보장했어. 그러나 트랜지언트 스토리지는 이 원칙의 위반을 허용하며, 잘못 사용하면 여러 호출에 걸쳐 사용될 때만 표면화되는 복잡한 버그로 이어질 수 있어.
간단한 예로 문제를 설명할게:
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.28;
contract MulService {
uint transient multiplier;
function setMultiplier(uint mul) external {
multiplier = mul;
}
function multiply(uint value) external view returns (uint) {
return value * multiplier;
}
}
그리고 외부 호출 시퀀스:
setMultiplier(42);
multiply(1);
multiply(2);
예시가 multiplier를 저장하는 데 memory나 storage를 사용했다면 완전히 합성 가능했을 거야. 시퀀스를 별도의 트랜잭션으로 나누든 어떤 방식으로 묶든 중요하지 않았을 거야. 항상 같은 결과를 얻었을 거야: multiplier가 42로 설정된 후 후속 호출은 각각 42와 84를 반환했을 거야. 이는 가스 비용을 줄이기 위해 여러 트랜잭션의 호출을 함께 배치하는 것 같은 사용 사례를 가능하게 해. 트랜지언트 스토리지는 합성 가능성을 더 이상 당연하게 여길 수 없으므로 그러한 사용 사례를 잠재적으로 깨뜨려. 예에서 호출이 같은 트랜잭션에서 실행되지 않으면 multiplier는 리셋되고 함수 multiply에 대한 다음 호출은 항상 0을 반환할 거야.
또 다른 예로, 트랜지언트 스토리지가 비교적 저렴한 키-값 저장소로 구성되므로, 스마트 컨트랙트 작성자는 매핑의 수정된 키를 추적하지 않고 따라서 호출 끝에 매핑을 지우지 않은 채 인메모리 매핑의 대체물로 트랜지언트 스토리지를 사용하고 싶은 유혹을 받을 수 있어. 그러나 이는 같은 트랜잭션에서 컨트랙트에 대한 이전 호출이 설정한 값이 남는 복잡한 트랜잭션에서 예상치 못한 동작으로 쉽게 이어질 수 있어. 컨트랙트에 대한 호출 프레임 끝에 지워지는 재진입 잠금에 트랜지언트 스토리지를 사용하는 것은 안전해. 그러나 재진입 잠금을 리셋하는 데 사용되는 100 가스를 아끼고자 하는 유혹을 반드시 물리쳐야 해. 그렇게 하지 않으면 컨트랙트가 트랜잭션 안에서 한 번의 호출만으로 제한되어, 체인 위 복잡한 애플리케이션의 초석이었던 복잡한 합성 트랜잭션에서의 사용을 막게 되니까. 일반적으로 스마트 컨트랙트에 대한 호출 끝에 트랜지언트 스토리지를 완전히 지워 이러한 종류의 문제를 피하고 복잡한 트랜잭션 안에서 컨트랙트 동작의 분석을 단순화하는 것이 권장돼. 자세한 내용은 EIP-1153의 Security Considerations 섹션을 확인해.
Constant와 Immutable 상태 변수 (Constant and Immutable State Variables)
상태 변수는 constant나 immutable로 선언될 수 있어. 두 경우 모두 컨트랙트가 구성된 후에는 변수를 수정할 수 없어. constant 변수의 경우 값이 컴파일 시점에 고정되어야 하고, immutable의 경우 생성 시점에 여전히 할당될 수 있어. 파일 수준에서 constant 변수를 정의하는 것도 가능해. 소스에서 그러한 변수의 모든 발생은 그 밑바탕 값으로 대체되고 컴파일러는 그것을 위해 storage 슬롯을 예약하지 않아. transient 키워드를 사용해 트랜지언트 스토리지에 슬롯을 할당받을 수도 없어.
일반 상태 변수에 비해 constant와 immutable 변수의 가스 비용은 훨씬 낮아. constant 변수의 경우 할당된 표현식이 접근되는 모든 곳에 복사되고 매번 재평가돼. 이는 지역 최적화를 허용해. immutable 변수는 생성 시점에 한 번 평가되고 그 값은 코드에서 접근되는 모든 곳에 복사돼. 이 값들에 대해 32바이트가 예약되며, 더 적은 바이트에 들어맞더라도 그래. 그 때문에 constant 값이 때로 immutable 값보다 더 저렴할 수 있어.
현재 constants와 immutables의 모든 타입이 구현된 것은 아니야. 지원되는 유일한 타입은 문자열(constant만)과 값 타입이야.
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.21;
uint constant X = 32**22 + 8;
contract C {
string constant TEXT = "abc";
bytes32 constant MY_HASH = keccak256("abc");
uint immutable decimals = 18;
uint immutable maxBalance;
address immutable owner = msg.sender;
constructor(uint decimals_, address ref) {
if (decimals_ != 0)
// Immutables are only immutable when deployed.
// At construction time they can be assigned to any number of times.
decimals = decimals_;
// Assignments to immutables can even access the environment.
maxBalance = ref.balance;
}
function isBalanceTooHigh(address other) public view returns (bool) {
return other.balance > maxBalance;
}
}
Constant
constant 변수의 경우 값은 컴파일 시점에 상수여야 하고 변수가 선언된 곳에서 할당되어야 해. 스토리지에 접근하거나, 블록체인 데이터(block.timestamp, address(this).balance, block.number 등) 또는 실행 데이터(msg.value나 gasleft())에 접근하거나, 외부 컨트랙트를 호출하는 어떤 표현식도 허용되지 않아. 메모리 할당에 부작용이 있을 수 있는 표현식은 허용되지만, 다른 메모리 객체에 부작용이 있을 수 있는 표현식은 허용되지 않아. 내장 함수 keccak256, sha256, ripemd160, ecrecover, addmod, mulmod는 허용돼(keccak256을 제외하고는 외부 컨트랙트를 호출하지만). 메모리 할당자에 대한 부작용을 허용하는 이유는 lookup-tables 같은 복잡한 객체를 구성하는 것이 가능해야 하기 때문이야. 이 기능은 아직 완전히 사용할 수 없어.
Immutable
immutable으로 선언된 변수는 constant로 선언된 것보다 약간 덜 제한돼: immutable 변수는 생성 시점에 값을 할당받을 수 있어. 값은 배포 전 언제든 바뀔 수 있고, 그 후 영구적이 돼. 한 가지 추가 제한은 immutables가 생성 후 실행될 가능성이 없는 표현식 안에서만 할당될 수 있다는 것이야. 이는 모든 수정자 정의와 생성자 외의 함수를 배제해. immutable 변수를 읽는 데는 제한이 없어. 변수가 처음 쓰이기 전에 읽는 것조차 허용되는데, Solidity의 변수는 항상 잘 정의된 초기 값을 가지기 때문이야. 그 때문에 immutable에 값을 명시적으로 할당하지 않는 것도 허용돼.
경고: 생성 시점에 immutables에 접근할 때 초기화 순서를 명심하세요. 명시적 초기화자를 제공해도, 특히 상속 계층의 다른 수준에 있을 때 일부 표현식이 그 초기화자 전에 평가될 수 있어요. 참고: Solidity 0.8.21 이전에는 immutable 변수의 초기화가 더 제한적이었어요. 그러한 변수는 생성 시점에 정확히 한 번 초기화되어야 했고 그 전에는 읽을 수 없었어요.
컴파일러가 생성한 컨트랙트 생성 코드는 immutables에 대한 모든 참조를 할당된 값으로 대체해, 컨트랙트의 런타임 코드가 반환되기 전에 수정할 거야. 컴파일러가 생성한 런타임 코드와 실제 블록체인에 저장된 것을 비교한다면 이것은 중요해. 컴파일러는 배포된 바이트코드에서 immutables가 어디에 위치하는지를 컴파일러 JSON 표준 출력의 immutableReferences 필드에 출력해.
커스텀 스토리지 배치 (Custom Storage Layout)
컨트랙트는 layout 지정자를 사용해 스토리지에 대한 임의의 위치를 정의할 수 있어. 기본 컨트랙트에서 상속된 것을 포함한 컨트랙트의 상태 변수는 기본 슬롯 0 대신 지정된 기본 슬롯에서 시작해.
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.29;
contract C layout at 0xAAAA + 0x11 {
uint[3] x; // Occupies slots 0xAABB..0xAABD
}
위 예에서 보여주듯 지정자는 layout at <base-slot-expression> 문법을 사용하고 컨트랙트 정의의 헤더에 위치해. layout 지정자는 상속 지정자 앞이나 뒤에 놓일 수 있고, 최대 한 번 나타날 수 있어. base-slot-expression은 컴파일 시점에 평가될 수 있고 uint256 범위의 값을 산출하는 정수 리터럴 표현식이어야 해. 그러한 표현식으로 초기화된 상수와 내장 함수 erc7201의 사용도 허용돼.
커스텀 layout은 컨트랙트의 스토리지가 "둘러싸서(wrap around)" 되게 만들 수 없어. 선택된 기본 슬롯이 정적 변수를 스토리지 끝을 넘어 밀어내면 컴파일러가 오류를 발행해. 동적 배열과 매핑의 데이터 영역은 배치가 선형이 아니므로 이 검사의 영향을 받지 않는다는 점을 참고해. 사용된 기본 슬롯과 무관하게 그 위치는 항상 uint256 범위 안에 놓이도록 계산되고 그 크기는 컴파일 시점에 알려지지 않아.
기본 슬롯에 다른 제한은 없지만, 주소 공간의 끝에 너무 가까운 위치는 피하는 것이 권장돼. 공간을 너무 적게 남기면 컨트랙트 업그레이드가 복잡해지거나, 인라인 어셈블리를 사용해 할당된 공간 너머에 추가 값을 저장하는 컨트랙트에 문제를 일으킬 수 있어. 스토리지 배치는 상속 트리의 최상위 컨트랙트에만 지정될 수 있으며, 그 트리의 모든 컨트랙트에서 모든 스토리지 변수의 위치에 영향을 줘. 변수는 정의 순서와 선형화된 상속 계층에서의 컨트랙트 위치에 따라 배치되며, 커스텀 기본 슬롯은 그 상대 위치를 보존해 모두 같은 양만큼 이동시켜. 스토리지 배치는 abstract 컨트랙트, 인터페이스, 라이브러리에는 지정될 수 없어. 또한 트랜지언트 상태 변수에는 영향을 주지 않는다는 것을 주목하는 것이 중요해. 스토리지 배치와 layout 지정자가 그것에 미치는 효과에 대한 자세한 내용은 스토리지 변수의 배치를 보세요.
경고:
layout과at식별자는 아직 언어에서 예약 키워드가 아니에요. 미래의 breaking 릴리스에서 예약될 것이므로 사용을 피하는 것이 강력히 권장돼요.
함수 (Functions)
함수는 컨트랙트 안과 밖에서 정의될 수 있어. 컨트랙트 밖의 함수("자유 함수(free functions)"라고도 함)는 항상 암시적 internal 가시성을 가져. 그 코드는 내부 라이브러리 함수와 유사하게 그것을 호출하는 모든 컨트랙트에 포함돼.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.1 <0.9.0;
function sum(uint[] memory arr) pure returns (uint s) {
for (uint i = 0; i < arr.length; i++)
s += arr[i];
}
contract ArrayExample {
bool found;
function f(uint[] memory arr) public {
// This calls the free function internally.
// The compiler will add its code to the contract.
uint s = sum(arr);
require(s >= 10);
found = true;
}
}
참고: 컨트랙트 밖에 정의된 함수도 항상 컨트랙트의 컨텍스트에서 실행돼요. 여전히 다른 컨트랙트를 호출하고, Ether를 보내고, 자신을 호출한 컨트랙트를 파괴할 수 있어요. 컨트랙트 안에 정의된 함수와의 주요 차이는 자유 함수가 변수
this, 스토리지 변수, 그리고 그 스코프에 없는 함수에 직접 접근하지 못한다는 것이에요.
함수 파라미터와 반환 변수 (Function Parameters and Return Variables)
함수는 타입화된 파라미터를 입력으로 받고, 다른 많은 언어와 달리 출력으로 임의의 수의 값을 반환할 수도 있어.
함수 파라미터 (Function Parameters)
함수 파라미터는 변수와 같은 방식으로 선언되고, 사용되지 않는 파라미터의 이름은 생략할 수 있어. 예를 들어 컨트랙트가 두 정수로 구성된 한 종류의 외부 호출을 받게 하려면 다음과 같이 사용해:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;
contract Simple {
uint sum;
function taker(uint a, uint b) public {
sum = a + b;
}
}
함수 파라미터는 다른 로컬 변수처럼 사용될 수 있고 할당될 수도 있어.
반환 변수 (Return Variables)
함수 반환 변수는 returns 키워드 뒤에 같은 문법으로 선언돼. 예를 들어 두 결과를 반환하려 한다고 해보자: 함수 파라미터로 전달된 두 정수의 합과 곱. 그러면 다음과 같이 사용해:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;
contract Simple {
function arithmetic(uint a, uint b)
public
pure
returns (uint sum, uint product)
{
sum = a + b;
product = a * b;
}
}
반환 변수의 이름은 생략될 수 있어. 반환 변수는 다른 로컬 변수처럼 사용될 수 있고 기본값으로 초기화되며 (재)할당될 때까지 그 값으로 유지돼. 반환 변수에 명시적으로 할당한 다음 위처럼 함수를 떠나거나, return 문장으로 반환 값(단일 또는 다중)을 직접 제공할 수 있어:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;
contract Simple {
function arithmetic(uint a, uint b)
public
pure
returns (uint sum, uint product)
{
return (a + b, a * b);
}
}
반환 변수를 가진 함수를 떠나기 위해 조기 return을 사용한다면 return 문장과 함께 반환 값을 제공해야 해.
참고: non-internal 함수에서 일부 타입은 반환할 수 없어. 여기에는 아래 나열된 타입과 재귀적으로 포함하는 복합 타입이 포함돼요:
- 매핑,
- 내부 함수 타입,
- 위치가
storage로 설정된 참조 타입,- 다차원 배열(ABI coder v1에만 적용),
- 구조체(ABI coder v1에만 적용). 이 제한은 내부 ABI가 다르기 때문에 라이브러리 함수에는 적용되지 않아요.
여러 값 반환 (Returning Multiple Values)
함수에 여러 반환 타입이 있으면 return (v0, v1, ..., vn) 문장을 사용해 여러 값을 반환할 수 있어. 구성 요소의 수는 반환 변수의 수와 같아야 하고, 타입은 잠재적 암시적 변환 후에 일치해야 해.
상태 가변성 (State Mutability)
View 함수 (View Functions)
함수는 view로 선언될 수 있는데, 그 경우 상태를 수정하지 않겠다고 약속해.
참고: 컴파일러의 EVM 대상이 Byzantium 이상(기본)이면 view 함수가 호출될 때
STATICCALLopcode가 사용되며, 이는 EVM 실행의 일부로 상태가 수정되지 않도록 강제해. 라이브러리 view 함수에는 결합된DELEGATECALL과STATICCALL이 없으므로DELEGATECALL이 사용돼. 즉 라이브러리 view 함수에는 상태 수정을 막는 런타임 검사가 없어. 라이브러리 코드는 보통 컴파일 시점에 알려지고 정적 검사기가 컴파일 시점 검사를 수행하므로 보안에 부정적 영향을 주지 않아야 해.
다음 문장들이 상태를 수정하는 것으로 간주돼:
- 상태 변수(storage와 transient storage)에 쓰기.
- 이벤트 방출.
- 다른 컨트랙트 생성.
selfdestruct사용.- 호출을 통해 Ether 보내기.
view나pure로 표시되지 않은 함수 호출.- 저수준 호출 사용.
- 특정 opcode를 포함하는 인라인 어셈블리 사용.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.0 <0.9.0;
contract C {
function f(uint a, uint b) public view returns (uint) {
return a * (b + 42) + block.timestamp;
}
}
참고: 함수의
constant는view의 별칭이었지만 0.5.0 버전에서 없어졌어요. 참고: getter 메서드는 자동으로view로 표시돼요. 참고: 0.5.0 버전 이전에는 컴파일러가 view 함수에STATICCALLopcode를 사용하지 않았어요. 이는 유효하지 않은 명시적 타입 변환을 통해 view 함수에서 상태 수정을 가능하게 했어요. view 함수에STATICCALL을 사용하면 상태 수정이 EVM 수준에서 방지돼요.
Pure 함수 (Pure Functions)
함수는 pure로 선언될 수 있는데, 그 경우 상태에서 읽거나 수정하지 않겠다고 약속해. 특히 pure 함수는 입력과 msg.data만 주어지고 현재 블록체인 상태에 대한 어떤 지식 없이 컴파일 시점에 평가할 수 있어야 해. 이는 immutable 변수에서 읽는 것이 non-pure 연산일 수 있음을 의미해.
참고: 컴파일러의 EVM 대상이 Byzantium 이상(기본)이면
STATICCALLopcode가 사용되며, 이는 상태가 읽히지 않는 것을 보장하지는 않지만 적어도 수정되지 않음을 보장해.
위에서 설명한 상태 수정 문장 목록에 더해, 다음은 상태에서 읽는 것으로 간주돼:
- 상태 변수(storage와 transient storage)에서 읽기.
address(this).balance나<address>.balance접근.block,tx,msg의 멤버 접근(msg.sig와msg.data제외).pure로 표시되지 않은 함수 호출.- 특정 opcode를 포함하는 인라인 어셈블리 사용.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.0 <0.9.0;
contract C {
function f(uint a, uint b) public pure returns (uint) {
return a * (b + 42);
}
}
Pure 함수는 오류가 발생할 때 잠재적 상태 변경을 되돌리기 위해 revert()와 require() 함수를 사용할 수 있어. 상태 변경을 되돌리는 것은 "상태 수정"으로 간주되지 않아. view나 pure 제한이 없던 코드에서 이전에 만든 변경만 되돌리고, 그 코드는 되돌림을 잡아 전달하지 않을 선택권이 있기 때문이야. 이 동작은 STATICCALL opcode와도 일치해.
경고: EVM 수준에서 함수가 상태를 읽지 못하게 하는 것은 불가능해요. 상태에 쓰지 못하게만 할 수 있어요(즉 EVM 수준에서
view만 강제할 수 있고pure는 강제할 수 없음). 참고: 0.5.0 버전 이전에는 컴파일러가 pure 함수에STATICCALLopcode를 사용하지 않았어요. 이는 유효하지 않은 명시적 타입 변환을 통해 pure 함수에서 상태 수정을 가능하게 했어요. pure 함수에STATICCALL을 사용하면 상태 수정이 EVM 수준에서 방지돼요. 참고: 0.4.17 버전 이전에는 컴파일러가 pure가 상태를 읽지 않는 것을 강제하지 않았어요. 이는 컴파일 타임 타입 검사이며, 컨트랙트 타입 사이의 유효하지 않은 명시적 변환으로 우회될 수 있어요. 컴파일러는 컨트랙트의 타입이 상태 변경 연산을 하지 않는 것을 검증할 수 있지만, 런타임에 호출될 컨트랙트가 실제로 그 타입인지는 검사할 수 없기 때문이에요.
특수 함수 (Special Functions)
Receive Ether 함수 (Receive Ether Function)
컨트랙트는 최대 하나의 receive 함수를 가질 수 있으며, receive() external payable { ... }(function 키워드 없이)로 선언돼. 이 함수는 인자를 가질 수 없고, 아무것도 반환할 수 없으며, external 가시성과 payable 상태 가변성을 가져야 해. virtual일 수 있고, override할 수 있고, 수정자를 가질 수 있어.
receive 함수는 빈 calldata로 컨트랙트에 대한 호출 시 실행돼. 이것은 일반 Ether 전송(예: .send() 또는 .transfer()를 통해)에서 실행되는 함수야. 그러한 함수가 없지만 payable fallback 함수가 있으면, 일반 Ether 전송에 fallback 함수가 호출돼. receive Ether도 payable fallback 함수도 없으면, 컨트랙트는 payable 함수 호출을 나타내지 않는 트랜잭션을 통해 Ether를 받을 수 없고 예외를 던져.
최악의 경우 receive 함수는 2300 가스만 사용할 수 있다고 의존할 수 있어(예: send나 transfer가 사용될 때), 기본 로깅 외의 다른 연산을 수행할 여지가 거의 없어. 다음 연산은 2300 가스 스티펜드를 초과하는 가스를 소비할 거야:
- 스토리지에 쓰기
- 컨트랙트 생성
- 많은 가스를 소비하는 외부 함수 호출
- Ether 보내기
경고:
send()와transfer()는 더 이상 사용되지 않고 제거될 예정이에요. 자세한 내용은 send와 transfer 섹션을 보세요. 경고: Ether가 함수 호출 없이 컨트랙트에 직접 보내질 때(즉 발신자가send나transfer를 사용) 수신 컨트랙트가 receive Ether 함수나 payable fallback 함수를 정의하지 않으면 예외가 던져지고 Ether가 돌려보내져요(Solidity v0.4.0 이전에는 달랐음). 컨트랙트가 Ether를 받게 하려면 receive Ether 함수를 구현해야 해(Ether를 받는 데 payable fallback 함수를 사용하는 것은 권장되지 않아. fallback이 호출되어 발신자의 인터페이스 혼동에 대해 실패하지 않기 때문). 경고: receive Ether 함수가 없는 컨트랙트는 coinbase 트랜잭션(별칭: 채굴자 블록 보상)의 수신자나selfdestruct의 대상으로서 Ether를 받을 수 있어. 컨트랙트는 그러한 Ether 전송에 반응할 수 없고 따라서 거부할 수도 없어. 이것은 EVM의 설계 선택이며 Solidity가 우회할 수 없어. 또한address(this).balance가 컨트랙트에서 구현된 수동 회계의 합보다 높을 수 있음을 의미해(즉 receive Ether 함수에서 갱신되는 카운터를 가지는 것).
아래에서 receive 함수를 사용하는 Sink 컨트랙트의 예를 볼 수 있어.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;
// This contract keeps all Ether sent to it with no way
// to get it back.
contract Sink {
event Received(address, uint);
receive() external payable {
emit Received(msg.sender, msg.value);
}
}
Fallback 함수 (Fallback Function)
컨트랙트는 최대 하나의 fallback 함수를 가질 수 있으며, fallback () external [payable] 또는 fallback (bytes calldata input) external [payable] returns (bytes memory output)(둘 다 function 키워드 없이)로 선언돼. 이 함수는 external 가시성을 가져야 해. fallback 함수는 virtual일 수 있고, override할 수 있고, 수정자를 가질 수 있어.
fallback 함수는 다른 함수 중 어떤 것도 주어진 함수 시그니처와 일치하지 않거나, 데이터가 전혀 공급되지 않고 receive Ether 함수가 없는 경우 컨트랙트에 대한 호출 시 실행돼. fallback 함수는 항상 데이터를 받지만, Ether도 받으려면 payable로 표시되어야 해. 파라미터가 있는 버전이 사용되면 input은 컨트랙트에 보내진 전체 데이터(msg.data와 같음)를 포함하고 output에 데이터를 반환할 수 있어. 반환된 데이터는 ABI 인코딩되지 않아. 대신 수정 없이(패딩조차 없이) 반환돼.
최악의 경우 payable fallback 함수가 receive 함수 대신 사용되면 2300 가스만 사용할 수 있다고 의존할 수 있어(receive Ether 함수에서 이것의 의미에 대한 간단한 설명을 보세요). 어떤 함수와 마찬가지로 fallback 함수는 충분한 가스가 전달되는 한 복잡한 연산을 실행할 수 있어.
경고: receive Ether 함수가 없으면 일반 Ether 전송에 대해서도 payable fallback 함수가 실행돼요. payable fallback 함수를 정의한다면 Ether 전송을 인터페이스 혼동과 구분하기 위해 receive Ether 함수도 항상 정의하는 것이 권장돼요. 참고: 입력 데이터를 디코딩하려면 함수 선택자에 대해 첫 네 바이트를 확인한 다음, 배열 슬라이스 문법과 함께
abi.decode를 사용해 ABI 인코딩된 데이터를 디코딩할 수 있어요:(c, d) = abi.decode(input[4:], (uint256, uint256));이는 최후의 수단으로만 사용해야 하고 대신 적절한 함수를 사용해야 한다는 점을 참고하세요.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.2 <0.9.0;
contract Test {
uint x;
// This function is called for all messages sent to
// this contract (there is no other function).
// Sending Ether to this contract will cause an exception,
// because the fallback function does not have the `payable`
// modifier.
fallback() external { x = 1; }
}
contract TestPayable {
uint x;
uint y;
// This function is called for all messages sent to
// this contract, except plain Ether transfers
// (there is no other function except the receive function).
// Any call with non-empty calldata to this contract will execute
// the fallback function (even if Ether is sent along with the call).
fallback() external payable { x = 1; y = msg.value; }
// This function is called for plain Ether transfers, i.e.
// for every call with empty calldata.
receive() external payable { x = 2; y = msg.value; }
}
contract Caller {
function callTest(Test test) public returns (bool) {
(bool success,) = address(test).call(abi.encodeWithSignature("nonExistingFunction()"));
require(success);
// results in test.x becoming == 1.
// address(test) will not allow to call ``send`` directly, since ``test`` has no payable
// fallback function.
// It has to be converted to the ``address payable`` type to even allow calling ``send`` on it.
address payable testPayable = payable(address(test));
// If someone sends Ether to that contract,
// the transfer will fail, i.e. this returns false here.
// This will report a warning (deprecation)
return testPayable.send(2 ether);
}
function callTestPayable(TestPayable test) public returns (bool) {
(bool success,) = address(test).call(abi.encodeWithSignature("nonExistingFunction()"));
require(success);
// results in test.x becoming == 1 and test.y becoming 0.
(success,) = address(test).call{value: 1}(abi.encodeWithSignature("nonExistingFunction()"));
require(success);
// results in test.x becoming == 1 and test.y becoming 1.
// If someone sends Ether to that contract, the receive function in TestPayable will be called.
// Since that function writes to storage, it takes more gas than is available with a
// simple ``send`` or ``transfer``. Because of that, we have to use a low-level call.
(success,) = address(test).call{value: 2 ether}("");
require(success);
// results in test.x becoming == 2 and test.y becoming 2 ether.
return true;
}
}
함수 오버로딩 (Function Overloading)
컨트랙트는 같은 이름을 가졌지만 파라미터 타입이 다른 여러 함수를 가질 수 있어. 이 과정을 "오버로딩(overloading)"이라고 하며 상속된 함수에도 적용돼. 다음 예는 컨트랙트 A의 스코프에서 함수 f의 오버로딩을 보여줘.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;
contract A {
function f(uint value) public pure returns (uint out) {
out = value;
}
function f(uint value, bool really) public pure returns (uint out) {
if (really)
out = value;
}
}
오버로드된 함수는 외부 인터페이스에도 존재해. 외부적으로 보이는 두 함수가 Solidity 타입으로는 다르지만 외부 타입으로는 다르지 않으면 오류야.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;
// This will not compile
contract A {
function f(B value) public pure returns (B out) {
out = value;
}
function f(address value) public pure returns (address out) {
out = value;
}
}
contract B {
}
위 두 f 오버로드는 Solidity 안에서는 다른 것으로 간주되지만 ABI에서는 둘 다 address 타입을 받게 돼.
오버로드 해석과 인자 매칭 (Overload resolution and Argument matching)
오버로드된 함수는 현재 스코프의 함수 선언을 함수 호출에 공급된 인자와 매칭함으로써 선택돼. 모든 인자가 예상 타입으로 암시적으로 변환될 수 있으면 함수가 오버로드 후보로 선택돼. 정확히 하나의 후보가 없으면 해석이 실패해.
참고: 반환 파라미터는 오버로드 해석에 고려되지 않아요.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;
contract A {
function f(uint8 val) public pure returns (uint8 out) {
out = val;
}
function f(uint256 val) public pure returns (uint256 out) {
out = val;
}
}
f(50) 호출은 50이 uint8과 uint256 타입 둘 다로 암시적으로 변환될 수 있으므로 타입 오류를 만들 거야. 반면 f(256)은 256이 uint8로 암시적으로 변환될 수 없으므로 f(uint256) 오버로드로 해석될 거야.
이벤트 (Events)
Solidity 이벤트는 EVM의 로깅 기능 위에 추상화를 제공해. 애플리케이션은 이더리움 클라이언트의 RPC 인터페이스를 통해 이러한 이벤트를 구독하고 들을 수 있어. 이벤트는 파일 수준이나 컨트랙트의 상속 가능한 멤버(인터페이스와 라이브러리 포함)로 정의될 수 있어. 호출하면 인자가 트랜잭션의 로그 - 블록체인의 특수 데이터 구조 - 에 저장되게 해. 이러한 로그는 그것을 방출한 컨트랙트의 주소와 연관되고, 블록체인에 통합되며, 블록이 접근 가능한 한(현재는 영원하지만 미래에 바뀔 수 있음) 거기에 남아.
Log와 그 이벤트 데이터는 컨트랙트 안에서(그것을 만든 컨트랙트에서조차) 접근할 수 없어. 로그에 대한 Merkle 증명을 요청하는 것은 가능하므로, 외부 엔티티가 컨트랙트에 그러한 증명을 공급하면 로그가 실제로 블록체인 안에 존재하는지 확인할 수 있어. 컨트랙트는 마지막 256개 블록 해시만 볼 수 있으므로 블록 헤더를 공급해야 해.
최대 세 개의 파라미터에 indexed 속성을 추가할 수 있으며, 이것은 로그의 데이터 부분 대신 "토픽(topics)"이라고 알려진 특수 데이터 구조에 추가돼. 토픽은 단일 워드(32바이트)만 담을 수 있으므로, indexed 인자에 참조 타입을 사용하면 값의 Keccak-256 해시가 대신 토픽으로 저장돼. indexed 속성이 없는 모든 파라미터는 로그의 데이터 부분에 ABI 인코딩돼.
토픽을 사용하면 이벤트를 검색할 수 있어. 예를 들어 일부 이벤트에 대한 블록 시퀀스를 필터링할 때. 특정 주소 값을 가진 토픽과 일치하는 로그를 필터링하는 데 web3.js subscribe("logs") 메서드를 사용하는 코드:
var options = {
fromBlock: 0,
address: web3.eth.defaultAccount,
topics: ["0x0000000000000000000000000000000000000000000000000000000000000000", null, null]
};
web3.eth.subscribe('logs', options, function (error, result) {
if (!error)
console.log(result);
})
.on("data", function (log) {
console.log(log);
})
.on("changed", function (log) {
});
이벤트의 시그니처 해시는 anonymous 지정자로 이벤트를 선언하지 않는 한 토픽 중 하나야. 즉 이름으로 특정 익명 이벤트를 필터링하는 것은 불가능하고, 컨트랙트 주소로만 필터링할 수 있어. 익명 이벤트의 장점은 배포하고 호출하는 것이 더 저렴하다는 것이야. 또한 세 개가 아닌 네 개의 indexed 인자를 선언할 수 있게 해.
참고: 트랜잭션 로그는 타입이 아닌 이벤트 데이터만 저장하므로, 데이터를 올바르게 해석하려면 어떤 파라미터가 indexed이고 이벤트가 anonymous인지 포함한 이벤트의 타입을 알아야 해요. 특히 익명 이벤트를 사용해 다른 이벤트의 시그니처를 "위조"하는 것이 가능해요.
이벤트의 멤버 (Members of Events)
event.selector: non-anonymous 이벤트의 경우, 기본 토픽에서 사용되는 이벤트 시그니처의 keccak256 해시를 담은bytes32값.
예시 (Example)
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.21 <0.9.0;
contract ClientReceipt {
event Deposit(
address indexed from,
bytes32 indexed id,
uint value
);
function deposit(bytes32 id) public payable {
// Events are emitted using `emit`, followed by
// the name of the event and the arguments
// (if any) in parentheses. Any such invocation
// (even deeply nested) can be detected from
// the JavaScript API by filtering for `Deposit`.
emit Deposit(msg.sender, id, msg.value);
}
}
JavaScript API에서의 사용은 다음과 같아:
var abi = /* abi as generated by the compiler */;
var ClientReceipt = web3.eth.contract(abi);
var clientReceipt = ClientReceipt.at("0x1234...ab67" /* address */);
var depositEvent = clientReceipt.Deposit();
// watch for changes
depositEvent.watch(function(error, result){
// result contains non-indexed arguments and topics
// given to the `Deposit` call.
if (!error)
console.log(result);
});
// Or pass a callback to start watching immediately
var depositEvent = clientReceipt.Deposit(function(error, result) {
if (!error)
console.log(result);
});
위의 출력은 다음과 같아(잘린):
{
"returnValues": {
"from": "0x1111…FFFFCCCC",
"id": "0x50…sd5adb20",
"value": "0x420042"
},
"raw": {
"data": "0x7f…91385",
"topics": ["0xfd4…b4ead7", "0x7f…1a91385"]
}
}
이벤트 이해를 위한 추가 자료 (Additional Resources for Understanding Events)
- JavaScript 문서
- 이벤트 사용 예시
- js에서 접근하는 방법
커스텀 에러 (Custom Errors)
Solidity의 에러는 사용자에게 연산이 왜 실패했는지 설명하는 편리하고 가스 효율적인 방법을 제공해. 컨트랙트 안과 밖(인터페이스와 라이브러리 포함)에서 정의될 수 있어. revert 문장이나 require 함수와 함께 사용해야 해. revert 문장 또는 조건이 거짓으로 평가된 require 호출의 경우, 현재 호출의 모든 변경이 되돌려지고 에러 데이터가 호출자에게 다시 전달돼.
아래 예는 함수 transferWithRevertError에서 revert 문장과 함께 커스텀 에러를 사용하는 것과, 함수 transferWithRequireError에서 require와 함께 하는 더 새로운 접근을 보여줘.
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.27;
/// Insufficient balance for transfer. Needed `required` but only
/// `available` available.
/// @param available balance available.
/// @param required requested amount to transfer.
error InsufficientBalance(uint256 available, uint256 required);
contract TestToken {
mapping(address => uint) balance;
function transferWithRevertError(address to, uint256 amount) public {
if (amount > balance[msg.sender])
revert InsufficientBalance({
available: balance[msg.sender],
required: amount
});
balance[msg.sender] -= amount;
balance[to] += amount;
}
function transferWithRequireError(address to, uint256 amount) public {
require(amount <= balance[msg.sender], InsufficientBalance(balance[msg.sender], amount));
balance[msg.sender] -= amount;
balance[to] += amount;
}
// ...
}
커스텀 에러와 함께 require를 사용할 때 언급할 또 다른 중요한 세부 사항은, 에러 기반 revert 이유에 대한 메모리 할당이 되돌리는 경우에만 발생한다는 것이야. 이는 상수와 문자열 리터럴의 최적화와 함께 if (!condition) revert CustomError(args) 패턴만큼 가스 효율적으로 만든다.
에러는 오버로드되거나 재정의될 수 없지만 상속된다. 같은 에러는 스코프가 구별되는 한 여러 곳에서 정의될 수 있어. 에러의 인스턴스는 revert 문장이나 require 함수의 두 번째 인자로만 생성될 수 있어. 에러는 외부 오프체인 컴포넌트로 반환되거나 try/catch 문장에서 잡히도록 revert 연산으로 호출자에게 전달되는 데이터를 만들어.
에러는 외부 호출에서 올 때만 잡을 수 있다는 점을 참고해. 내부 호출이나 같은 함수 안에서 일어나는 되돌림은 잡을 수 없어. 어떤 파라미터도 제공하지 않으면 에러는 4바이트의 데이터만 필요하고, 위처럼 NatSpec을 사용해 에러 뒤의 이유를 더 설명할 수 있으며, 이는 체인에 저장되지 않아. 이는 동시에 아주 저렴하고 편리한 에러 보고 기능으로 만들어.
더 구체적으로 에러 인스턴스는 같은 이름과 타입의 함수에 대한 함수 호출과 같은 방식으로 ABI 인코딩되고 revert opcode의 반환 데이터로 사용돼. 즉 데이터는 4바이트 선택자와 ABI 인코딩된 데이터로 구성돼. 선택자는 에러 타입의 시그니처의 keccak256-해시의 첫 네 바이트로 구성돼.
참고: 컨트랙트가 같은 이름의 다른 에러나, 호출자가 구별할 수 없는 다른 곳에 정의된 에러로 되돌리는 것이 가능해요. 외부, 즉 ABI의 경우 에러의 이름만 관련이 있고 정의된 컨트랙트나 파일은 관련이 없어요.
require(condition, "description");문장은error Error(string)을 정의할 수 있다면if (!condition) revert Error("description")과 동등할 거야. 그러나Error는 내장 타입이고 사용자 제공 코드에서 정의될 수 없다는 점을 참고해. 마찬가지로 실패한 assert나 유사한 조건은 내장 타입Panic(uint256)의 에러로 되돌아가. 참고: 에러 데이터는 실패의 표시만 제공해야 하고 제어 흐름의 수단으로는 사용하면 안 돼. 내부 호출의 revert 데이터가 기본적으로 외부 호출 체인을 통해 다시 전파되기 때문이야. 즉 내부 호출이 호출한 컨트랙트에서 온 것처럼 보이는 revert 데이터를 "위조"할 수 있어.
에러의 멤버 (Members of Errors)
error.selector: 에러 선택자를 담은bytes4값.
상속 (Inheritance)
Solidity는 다형성을 포함한 다중 상속을 지원해. 다형성은 함수 호출(내부 및 외부)이 상속 계층에서 가장 많이 파생된 컨트랙트의 같은 이름(및 파라미터 타입)의 함수를 항상 실행한다는 뜻이야. 이것은 virtual과 override 키워드를 사용해 계층의 각 함수에서 명시적으로 활성화되어야 해. 자세한 내용은 함수 재정의를 보세요.
ContractName.functionName()로 컨트랙트를 명시적으로 지정하거나, 평탄화된 상속 계층에서 한 단계 위의 함수를 호출하려면 super.functionName()를 사용해 상속 계층의 더 위에 있는 함수를 내부적으로 호출할 수 있어(아래 참고).
컨트랙트가 다른 컨트랙트에서 상속받을 때 블록체인에는 단일 컨트랙트만 생성되고, 모든 기본 컨트랙트의 코드가 생성된 컨트랙트로 컴파일돼. 즉 기본 컨트랙트의 함수에 대한 모든 내부 호출도 내부 함수 호출만 사용해(super.f(..)는 메시지 호출이 아닌 JUMP를 사용). 상태 변수 섀도잉(shadowing)은 오류로 간주돼. 파생 컨트랙트는 어떤 기본 컨트랙트에도 같은 이름의 보이는 상태 변수가 없을 때만 상태 변수 x를 선언할 수 있어.
일반 상속 시스템은 특히 다중 상속에 관해 Python의 것과 매우 유사하지만, 몇 가지 차이도 있어. 다음 예에서 세부 사항이 주어져.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract Owned {
address payable owner;
constructor() { owner = payable(msg.sender); }
}
// Use `is` to derive from another contract. Derived
// contracts can access all non-private members including
// internal functions and state variables. These cannot be
// accessed externally via `this`, though.
contract Emittable is Owned {
event Emitted();
// The keyword `virtual` means that the function can change
// its behavior in derived classes ("overriding").
function emitEvent() virtual public {
if (msg.sender == owner)
emit Emitted();
}
}
// These abstract contracts are only provided to make the
// interface known to the compiler. Note the function
// without body. If a contract does not implement all
// functions it can only be used as an interface.
abstract contract Config {
function lookup(uint id) public virtual returns (address adr);
}
abstract contract NameReg {
function register(bytes32 name) public virtual;
function unregister() public virtual;
}
// Multiple inheritance is possible. Note that `Owned` is
// also a base class of `Emittable`, yet there is only a single
// instance of `Owned` (as for virtual inheritance in C++).
contract Named is Owned, Emittable {
constructor(bytes32 name) {
Config config = Config(0xD5f9D8D94886E70b06E474c3fB14Fd43E2f23970);
NameReg(config.lookup(1)).register(name);
}
// Functions can be overridden by another function with the same name and
// the same number/types of inputs. If the overriding function has different
// types of output parameters, that causes an error.
// Both local and message-based function calls take these overrides
// into account.
// If you want the function to override, you need to use the
// `override` keyword. You need to specify the `virtual` keyword again
// if you want this function to be overridden again.
function emitEvent() public virtual override {
if (msg.sender == owner) {
Config config = Config(0xD5f9D8D94886E70b06E474c3fB14Fd43E2f23970);
NameReg(config.lookup(1)).unregister();
// It is still possible to call a specific
// overridden function.
Emittable.emitEvent();
}
}
}
// If a constructor takes an argument, it needs to be
// provided in the header or modifier-invocation-style at
// the constructor of the derived contract (see below).
contract PriceFeed is Owned, Emittable, Named("GoldFeed") {
uint info;
function updateInfo(uint newInfo) public {
if (msg.sender == owner) info = newInfo;
}
// Here, we only specify `override` and not `virtual`.
// This means that contracts deriving from `PriceFeed`
// cannot change the behavior of `emitEvent` anymore.
function emitEvent() public override(Emittable, Named) { Named.emitEvent(); }
function get() public view returns(uint r) { return info; }
}
위에서 우리는 Emittable.emitEvent()를 호출해 "전달"하는데, 다음 예에서 보여주듯 이 방식은 문제가 돼:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract Owned {
address payable owner;
constructor() { owner = payable(msg.sender); }
}
contract Emittable is Owned {
event Emitted();
function emitEvent() virtual public {
if (msg.sender == owner) {
emit Emitted();
}
}
}
contract Base1 is Emittable {
event Base1Emitted();
function emitEvent() public virtual override {
/* Here, we emit an event to simulate some Base1 logic */
emit Base1Emitted();
Emittable.emitEvent();
}
}
contract Base2 is Emittable {
event Base2Emitted();
function emitEvent() public virtual override {
/* Here, we emit an event to simulate some Base2 logic */
emit Base2Emitted();
Emittable.emitEvent();
}
}
contract Final is Base1, Base2 {
event FinalEmitted();
function emitEvent() public override(Base1, Base2) {
/* Here, we emit an event to simulate some Final logic */
emit FinalEmitted();
Base2.emitEvent();
}
}
Final.emitEvent()에 대한 호출은 최종 override에서 명시적으로 지정하므로 Base2.emitEvent를 호출할 거지만, 이 함수는 Base1.emitEvent를 우회해서 다음 이벤트 시퀀스를 만들 거야: FinalEmitted -> Base2Emitted -> Emitted, 예상 시퀀스 FinalEmitted -> Base2Emitted -> Base1Emitted -> Emitted 대신. 이것을 우회하는 방법은 super를 사용하는 것이야:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract Owned {
address payable owner;
constructor() { owner = payable(msg.sender); }
}
contract Emittable is Owned {
event Emitted();
function emitEvent() virtual public {
if (msg.sender == owner) {
emit Emitted();
}
}
}
contract Base1 is Emittable {
event Base1Emitted();
function emitEvent() public virtual override {
/* Here, we emit an event to simulate some Base1 logic */
emit Base1Emitted();
super.emitEvent();
}
}
contract Base2 is Emittable {
event Base2Emitted();
function emitEvent() public virtual override {
/* Here, we emit an event to simulate some Base2 logic */
emit Base2Emitted();
super.emitEvent();
}
}
contract Final is Base1, Base2 {
event FinalEmitted();
function emitEvent() public override(Base1, Base2) {
/* Here, we emit an event to simulate some Final logic */
emit FinalEmitted();
super.emitEvent();
}
}
Final이 super의 함수를 호출하면, 단순히 기본 컨트랙트 중 하나에서 이 함수를 호출하지 않아. 대신 최종 상속 그래프의 다음 기본 컨트랙트에서 이 함수를 호출하므로 Base1.emitEvent()를 호출할 거야(최종 상속 시퀀스는 가장 많이 파생된 컨트랙트부터 시작: Final, Base2, Base1, Emittable, Owned). super를 사용할 때 실제로 호출되는 함수는 그 타입이 알려져 있지만 사용되는 클래스의 컨텍스트에서는 알려지지 않아. 이것은 일반 가상 메서드 조회와 유사해.
함수 재정의 (Function Overriding)
기본 함수는 virtual로 표시되면 상속 컨트랙트가 그 동작을 바꾸도록 재정의할 수 있어. 재정의 함수는 함수 헤더에 override 키워드를 사용해야 해. 재정의 함수는 재정의된 함수의 가시성을 external에서 public으로만 바꿀 수 있어. 가변성은 다음 순서에 따라 더 엄격한 것으로 바뀔 수 있어: nonpayable은 view와 pure로 재정의될 수 있어. view는 pure로 재정의될 수 있어. payable은 예외이며 다른 어떤 가변성으로도 바뀔 수 없어.
다음 예는 가변성과 가시성의 변경을 보여줘:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract Base
{
function foo() virtual external view {}
}
contract Middle is Base {}
contract Inherited is Middle
{
function foo() override public pure {}
}
다중 상속의 경우 같은 함수를 정의하는 가장 많이 파생된 기본 컨트랙트를 override 키워드 뒤에 명시적으로 지정해야 해. 다시 말해 같은 함수를 정의하고 아직 다른 기본 컨트랙트에 의해 재정의되지 않은 모든 기본 컨트랙트를(상속 그래프의 어떤 경로를 통해) 지정해야 해. 추가로 컨트랙트가 (관련 없는) 여러 기본 컨트랙트에서 같은 함수를 상속받으면 명시적으로 재정의해야 해:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;
contract Base1
{
function foo() virtual public {}
}
contract Base2
{
function foo() virtual public {}
}
contract Inherited is Base1, Base2
{
// Derives from multiple bases defining foo(), so we must explicitly
// override it
function foo() public override(Base1, Base2) {}
}
함수가 공통 기본 컨트랙트에 정의되어 있거나, 공통 기본 컨트랙트에 이미 다른 모든 함수를 재정의하는 고유한 함수가 있다면 명시적 override 지정자는 필요하지 않아.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;
contract A { function f() public pure{} }
contract B is A {}
contract C is A {}
// No explicit override required
contract D is B, C {}
더 공식적으로, 시그니처에 대한 모든 override 경로의 일부인 기본 컨트랙트가 있고, (1) 그 기본이 함수를 구현하고 현재 컨트랙트에서 그 기본으로 가는 어떤 경로도 그 시그니처를 가진 함수를 언급하지 않거나, (2) 그 기본이 함수를 구현하지 않고 현재 컨트랙트에서 그 기본으로 가는 모든 경로에서 함수가 최대 한 번 언급된다면, 여러 기본에서 (직접 또는 간접적으로) 상속된 함수를 재정의할 필요가 없다. 이런 의미에서 시그니처에 대한 override 경로는 상속 그래프를 통해 현재 고려 중인 컨트랙트에서 시작해 재정의하지 않는 그 시그니처를 가진 함수를 언급하는 컨트랙트에서 끝나는 경로야.
재정의하는 함수를 virtual로 표시하지 않으면 파생 컨트랙트는 더 이상 그 함수의 동작을 바꿀 수 없어.
참고:
private가시성을 가진 함수는virtual일 수 없어요. 참고: 구현이 없는 함수는 인터페이스 밖에서virtual로 표시되어야 해요. 인터페이스에서는 모든 함수가 자동으로virtual로 간주돼요. 참고: Solidity 0.8.8부터 함수가 여러 기본에 정의된 경우를 제외하고는 인터페이스 함수를 재정의할 때override키워드가 필요하지 않아요.
함수의 파라미터와 반환 타입이 변수의 getter 함수와 일치하면 public 상태 변수가 외부 함수를 재정의할 수 있어:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;
contract A
{
function f() external view virtual returns(uint) { return 5; }
}
contract B is A
{
uint public override f;
}
참고: public 상태 변수가 외부 함수를 재정의할 수는 있지만, 그들 자신은 재정의될 수 없어요.
수정자 재정의 (Modifier Overriding) (deprecated)
함수 수정자는 서로 재정의할 수 있어. 이것은 함수 재정의와 같은 방식으로 작동해(수정자에는 오버로딩이 없다는 것 제외). 재정의된 수정자에 virtual 키워드를, 재정의하는 수정자에 override 키워드를 사용해야 해:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;
contract Base
{
// This will report a warning (deprecation)
modifier foo() virtual {_;}
}
contract Inherited is Base
{
modifier foo() override {_;}
}
다중 상속의 경우 모든 직접 기본 컨트랙트를 명시적으로 지정해야 해:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;
contract Base1
{
// This will report a warning (deprecation)
modifier foo() virtual {_;}
}
contract Base2
{
// This will report a warning (deprecation)
modifier foo() virtual {_;}
}
contract Inherited is Base1, Base2
{
modifier foo() override(Base1, Base2) {_;}
}
경고: virtual 수정자는 더 이상 사용되지 않고 제거될 예정이에요.
생성자 (Constructors)
생성자는 constructor 키워드로 선언된 선택적 함수이며, 컨트랙트 생성 시 실행되고 컨트랙트 초기화 코드를 실행할 수 있어. 생성자 코드가 실행되기 전에 상태 변수는 인라인으로 초기화하면 지정된 값으로, 그렇지 않으면 기본값으로 초기화돼. 생성자가 실행된 후 컨트랙트의 최종 코드가 블록체인에 배포돼. 코드 배포는 코드 길이에 선형적인 추가 가스를 소비해.
이 코드는 public 인터페이스의 일부인 모든 함수와 그곳에서 함수 호출을 통해 도달할 수 있는 모든 함수를 포함해. 생성자 코드나 생성자에서만 호출되는 내부 함수는 포함하지 않아. 생성자가 없으면 컨트랙트는 constructor() {}와 동등한 기본 생성자를 가정할 거야. 예를 들어:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
abstract contract A {
uint public a;
constructor(uint a_) {
a = a_;
}
}
contract B is A(1) {
constructor() {}
}
생성자에서 내부 파라미터(예: 스토리지 포인터)를 사용할 수 있어. 이 경우 그러한 파라미터는 외부에서 유효한 값을 할당받을 수 없고 파생 컨트랙트의 생성자를 통해서만 할당될 수 있으므로 컨트랙트를 abstract로 표시해야 해.
경고: 0.4.22 버전 이전에는 생성자가 컨트랙트와 같은 이름을 가진 함수로 정의됐어요. 이 문법은 더 이상 사용되지 않았고 0.5.0 버전에서 더 이상 허용되지 않아요. 경고: 0.7.0 버전 이전에는 생성자의 가시성을
internal또는public으로 지정해야 했어요.
기본 생성자의 인자 (Arguments for Base Constructors)
모든 기본 컨트랙트의 생성자는 아래에서 설명하는 선형화 규칙에 따라 호출돼. 기본 생성자에 인자가 있으면 파생 컨트랙트가 모두 지정해야 해. 이것은 두 가지 방법으로 할 수 있어:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract Base {
uint x;
constructor(uint x_) { x = x_; }
}
// Either directly specify in the inheritance list...
contract Derived1 is Base(7) {
constructor() {}
}
// or through a "modifier" of the derived constructor...
contract Derived2 is Base {
constructor(uint y) Base(y * y) {}
}
// or declare abstract...
abstract contract Derived3 is Base {
}
// and have the next concrete derived contract initialize it.
contract DerivedFromDerived is Derived3 {
constructor() Base(10 + 10) {}
}
한 가지 방법은 상속 목록에 직접(is Base(7)) 지정하는 것이야. 다른 방법은 파생 생성자의 일부로 수정자가 호출되는 방식(Base(y * y))이야. 첫 번째 방법은 생성자 인자가 상수이고 컨트랙트의 동작을 정의하거나 설명할 때 더 편리해. 두 번째 방법은 기본의 생성자 인자가 파생 컨트랙트의 것에 의존할 때 사용해야 해. 인자는 상속 목록이나 파생 생성자의 수정자 스타일 중 하나로 주어져야 해. 두 곳 모두에서 지정하는 것은 오류야.
파생 컨트랙트가 모든 기본 컨트랙트의 생성자에 인자를 지정하지 않으면 abstract로 선언되어야 해. 그 경우 다른 컨트랙트가 그것에서 파생될 때, 그 다른 컨트랙트의 상속 목록이나 생성자가 파라미터가 지정되지 않은 모든 기본 클래스에 필요한 파라미터를 제공해야 해(그렇지 않으면 그 다른 컨트랙트도 abstract로 선언되어야 함). 예를 들어 위 코드 조각에서 Derived3와 DerivedFromDerived를 보세요.
다중 상속과 선형화 (Multiple Inheritance and Linearization)
다중 상속을 허용하는 언어는 여러 문제를 다뤄야 해. 하나는 다이아몬드 문제(Diamond Problem)야. Solidity는 Python과 유사하게 C3 Linearization을 사용해 기본 클래스의 방향성 비순환 그래프(DAG)에서 특정 순서를 강제해. 이것은 바람직한 단조성(monotonicity) 속성을 결과로 내지만 일부 상속 그래프를 금지해. 특히 is 지시문에서 기본 클래스가 주어지는 순서가 중요해: 직접 기본 컨트랙트를 "가장 기본적인 것"부터 "가장 파생된 것" 순서로 나열해야 해. 이 순서가 Python에서 사용되는 것의 반대라는 점을 참고해.
이것을 설명하는 또 다른 단순화 방법은 여러 컨트랙트에서 정의된 함수가 호출될 때, 주어진 기본이 오른쪽에서 왼쪽으로(Python에서는 왼쪽에서 오른쪽) 깊이 우선 방식으로 검색되어 첫 일치에서 멈춘다는 것이야. 기본 컨트랙트가 이미 검색됐으면 건너뛰어. 다음 코드에서 Solidity는 "Linearization of inheritance graph impossible" 오류를 줄 거야.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;
contract X {}
contract A is X {}
// This will not compile
contract C is A, X {}
이유는 C가 X가 A를 재정의하도록 요청하고(이 순서로 A, X를 지정), 그러나 A 자신이 X를 재정의하도록 요청하는데, 이것은 해결할 수 없는 모순이기 때문이야.
고유한 재정의 없이 여러 기본에서 상속된 함수를 명시적으로 재정의해야 한다는 사실 때문에, C3 linearization은 실제로 그렇게 중요하지 않아. 상속 선형화가 특히 중요하고 아마 덜 명확한 한 영역은 상속 계층에 여러 생성자가 있을 때야. 생성자는 상속 컨트랙트의 생성자에서 인자가 제공되는 순서와 무관하게 항상 선형화된 순서로 실행돼. 예를 들어:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract Base1 {
constructor() {}
}
contract Base2 {
constructor() {}
}
// Constructors are executed in the following order:
// 1 - Base1
// 2 - Base2
// 3 - Derived1
contract Derived1 is Base1, Base2 {
constructor() Base1() Base2() {}
}
// Constructors are executed in the following order:
// 1 - Base2
// 2 - Base1
// 3 - Derived2
contract Derived2 is Base2, Base1 {
constructor() Base2() Base1() {}
}
// Constructors are still executed in the following order:
// 1 - Base2
// 2 - Base1
// 3 - Derived3
contract Derived3 is Base2, Base1 {
constructor() Base1() Base2() {}
}
같은 이름의 다른 종류의 멤버 상속 (Inheriting Different Kinds of Members of the Same Name)
상속으로 인해 컨트랙트가 같은 이름을 공유하는 여러 정의를 포함할 수 있는 유일한 상황은 다음과 같아:
- 함수의 오버로딩.
- virtual 함수의 재정의.
- 상태 변수 getter에 의한 external virtual 함수의 재정의.
- virtual 수정자의 재정의.
- 이벤트의 오버로딩.
추상 컨트랙트 (Abstract Contracts)
컨트랙트는 함수 중 적어도 하나가 구현되지 않았거나, 모든 기본 컨트랙트 생성자에 인자를 제공하지 않을 때 abstract로 표시되어야 해. 그렇지 않아도 컨트랙트는 여전히 abstract로 표시될 수 있는데, 예를 들어 컨트랙트가 직접 생성되지 않도록 의도하지 않을 때. 추상 컨트랙트는 인터페이스와 유사하지만, 인터페이스는 선언할 수 있는 것에서 더 제한적이야.
추상 컨트랙트는 다음 예와 같이 abstract 키워드로 선언돼. 이 컨트랙트가 abstract로 정의되어야 한다는 점을 참고해. 함수 utterance()가 선언됐지만 구현이 제공되지 않았기 때문이야(구현 본문 { }가 주어지지 않았음).
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;
abstract contract Feline {
function utterance() public virtual returns (bytes32);
}
그러한 추상 컨트랙트는 직접 인스턴스화될 수 없어. 추상 컨트랙트 자체가 모든 정의된 함수를 구현하더라도 마찬가지야. 기본 클래스로 추상 컨트랙트를 사용하는 것은 다음 예에서 보여줘:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;
abstract contract Feline {
function utterance() public pure virtual returns (bytes32);
}
contract Cat is Feline {
function utterance() public pure override returns (bytes32) { return "miaow"; }
}
컨트랙트가 추상 컨트랙트에서 상속받고 재정의를 통해 구현되지 않은 모든 함수를 구현하지 않으면, 그것도 abstract로 표시해야 해. 구현이 없는 함수는 문법이 매우 비슷하지만 함수 타입과 다르다는 점을 참고해. 구현 없는 함수(함수 선언)의 예:
function foo(address) external returns (address);
타입이 함수 타입인 변수 선언의 예:
function(address) external returns (address) foo;
추상 컨트랙트는 컨트랙트의 정의를 구현과 분리해 더 나은 확장성과 자기 문서화를 제공하고, Template method 같은 패턴을 용이하게 하며 코드 중복을 제거해. 추상 컨트랙트는 인터페이스에서 메서드를 정의하는 것이 유용한 것과 같은 방식으로 유용해. 추상 컨트랙트 설계자가 "내 자식들은 이 메서드를 구현해야 한다"고 말하는 방법이야.
참고: 추상 컨트랙트는 구현된 virtual 함수를 구현되지 않은 것으로 재정의할 수 없어요.
인터페이스 (Interfaces)
인터페이스는 추상 컨트랙트와 유사하지만, 어떤 함수도 구현할 수 없어. 추가 제한이 있어:
- 다른 컨트랙트에서 상속받을 수 없지만, 다른 인터페이스에서는 상속받을 수 있어.
- 선언된 모든 함수는 컨트랙트에서 public이어도 인터페이스에서
external이어야 해. - 생성자를 선언할 수 없어.
- 상태 변수를 선언할 수 없어.
- 수정자를 선언할 수 없어.
이러한 제한 중 일부는 미래에 완화될 수 있어. 인터페이스는 기본적으로 Contract ABI가 나타낼 수 있는 것으로 제한되며, ABI와 인터페이스 사이의 변환은 정보 손실 없이 가능해야 해. 인터페이스는 자체 키워드로 표시돼:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.2 <0.9.0;
interface Token {
enum TokenType { Fungible, NonFungible }
struct Coin { string obverse; string reverse; }
function transfer(address recipient, uint amount) external;
}
컨트랙트는 다른 컨트랙트를 상속받듯 인터페이스를 상속받을 수 있어. 인터페이스에 선언된 모든 함수는 암시적으로 virtual이고, 이것을 재정의하는 어떤 함수도 override 키워드가 필요하지 않아. 이것이 재정의 함수가 다시 재정의될 수 있다는 것을 자동으로 의미하지는 않아. 재정의 함수가 virtual로 표시된 경우에만 가능해.
인터페이스는 다른 인터페이스에서 상속받을 수 있어. 이것은 일반 상속과 같은 규칙을 가져.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.2 <0.9.0;
interface ParentA {
function test() external returns (uint256);
}
interface ParentB {
function test() external returns (uint256);
}
interface SubInterface is ParentA, ParentB {
// Must redefine test in order to assert that the parent
// meanings are compatible.
function test() external override(ParentA, ParentB) returns (uint256);
}
인터페이스와 다른 컨트랙트 유사 구조 안에 정의된 타입은 다른 컨트랙트에서 접근될 수 있어: Token.TokenType 또는 Token.Coin.
경고: 인터페이스는 Solidity 버전 0.5.0부터 enum 타입을 지원해요. pragma 버전이 이 버전을 최소로 지정하는지 확인하세요.
라이브러리 (Libraries)
라이브러리는 컨트랙트와 유사하지만, 그 목적은 특정 주소에서 한 번만 배포되고 그 코드가 EVM의 DELEGATECALL(Homestead까지는 CALLCODE) 기능을 사용해 재사용된다는 것이야. 즉 라이브러리 함수가 호출되면 그 코드가 호출하는 컨트랙트의 컨텍스트에서 실행된다는 뜻이야. 즉 this가 호출하는 컨트랙트를 가리키고, 특히 호출하는 컨트랙트의 스토리지에 접근할 수 있어. 라이브러리는 격리된 소스 코드 조각이므로, 명시적으로 공급되지 않으면(그렇지 않으면 이름을 붙일 방법이 없음) 호출하는 컨트랙트의 상태 변수에 접근할 수 없어. 라이브러리 함수는 상태를 수정하지 않는 경우(즉 view나 pure 함수인 경우)에만 직접 호출될 수 있어(DELEGATECALL 없이). 라이브러리는 무상태라고 가정되기 때문이야. 특히 라이브러리를 파괴하는 것은 불가능해.
참고: 0.4.20 버전까지는 Solidity의 타입 시스템을 우회해 라이브러리를 파괴하는 것이 가능했어요. 그 버전부터 라이브러리는 상태를 수정하는 함수가 직접 호출되는 것을(즉 DELEGATECALL 없이) 금지하는 메커니즘을 포함해요.
라이브러리는 그것을 사용하는 컨트랙트의 암시적 기본 컨트랙트로 볼 수 있어. 상속 계층에서 명시적으로 보이지는 않지만, 라이브러리 함수에 대한 호출은 명시적 기본 컨트랙트의 함수에 대한 호출처럼 보여(L.f() 같은 정규화된 접근 사용). 물론 내부 함수에 대한 호출은 내부 호출 규약을 사용하며, 이는 모든 내부 타입이 전달될 수 있고 memory에 저장된 타입이 복사되지 않고 참조로 전달된다는 뜻이야.
이것을 EVM에서 실현하려면, 컨트랙트에서 호출되는 내부 라이브러리 함수와 거기에서 호출되는 모든 함수의 코드는 컴파일 시점에 호출하는 컨트랙트에 포함되고, DELEGATECALL 대신 일반 JUMP 호출이 사용돼.
참고: 상속 비유는 public 함수에 이르면 깨져요.
L.f()로 public 라이브러리 함수를 호출하는 것은 외부 호출(정확히는 DELEGATECALL)을 결과로 내요. 대조적으로A.f()는A가 현재 컨트랙트의 기본 컨트랙트일 때 내부 호출이에요.
다음 예는 라이브러리를 사용하는 방법을 보여줘(하지만 수동 메서드를 사용. 집합을 구현하는 더 고급 예는 using for를 확인하세요).
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;
// We define a new struct datatype that will be used to
// hold its data in the calling contract.
struct Data {
mapping(uint => bool) flags;
}
library Set {
// Note that the first parameter is of type "storage
// reference" and thus only its storage address and not
// its contents is passed as part of the call. This is a
// special feature of library functions. It is idiomatic
// to call the first parameter `self`, if the function can
// be seen as a method of that object.
function insert(Data storage self, uint value)
public
returns (bool)
{
if (self.flags[value])
return false; // already there
self.flags[value] = true;
return true;
}
function remove(Data storage self, uint value)
public
returns (bool)
{
if (!self.flags[value])
return false; // not there
self.flags[value] = false;
return true;
}
function contains(Data storage self, uint value)
public
view
returns (bool)
{
return self.flags[value];
}
}
contract C {
Data knownValues;
function register(uint value) public {
// The library functions can be called without a
// specific instance of the library, since the
// "instance" will be the current contract.
require(Set.insert(knownValues, value));
}
// In this contract, we can also directly access knownValues.flags, if we want.
}
물론 라이브러리를 사용하는 데 이 방식을 따를 필요는 없어. struct 데이터 타입을 정의하지 않고도 사용할 수 있어. 함수는 storage 참조 파라미터 없이도 작동하고, 여러 storage 참조 파라미터를 어떤 위치에도 가질 수 있어.
Set.contains, Set.insert, Set.remove에 대한 호출은 모두 외부 컨트랙트/라이브러리에 대한 호출(DELEGATECALL)로 컴파일돼. 라이브러리를 사용하면 실제 외부 함수 호출이 수행된다는 것을 알아야 해. 다만 이 호출에서 msg.sender, msg.value, this는 그 값을 유지해(Homestead 이전에는 CALLCODE 사용 때문에 msg.sender와 msg.value가 바뀌었지만).
다음 예는 memory에 저장된 타입과 내부 함수를 라이브러리에서 사용해 외부 함수 호출의 오버헤드 없이 커스텀 타입을 구현하는 방법을 보여줘:
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;
struct bigint {
uint[] limbs;
}
library BigInt {
function fromUint(uint x) internal pure returns (bigint memory r) {
r.limbs = new uint[](1);
r.limbs[0] = x;
}
function add(bigint memory a, bigint memory b) internal pure returns (bigint memory r) {
r.limbs = new uint[](max(a.limbs.length, b.limbs.length));
uint carry = 0;
for (uint i = 0; i < r.limbs.length; ++i) {
uint limbA = limb(a, i);
uint limbB = limb(b, i);
unchecked {
r.limbs[i] = limbA + limbB + carry;
if (limbA + limbB < limbA || (limbA + limbB == type(uint).max && carry > 0))
carry = 1;
else
carry = 0;
}
}
if (carry > 0) {
// too bad, we have to add a limb
uint[] memory newLimbs = new uint[](r.limbs.length + 1);
uint i;
for (i = 0; i < r.limbs.length; ++i)
newLimbs[i] = r.limbs[i];
newLimbs[i] = carry;
r.limbs = newLimbs;
}
}
function limb(bigint memory a, uint index) internal pure returns (uint) {
return index < a.limbs.length ? a.limbs[index] : 0;
}
function max(uint a, uint b) private pure returns (uint) {
return a > b ? a : b;
}
}
contract C {
using BigInt for bigint;
function f() public pure {
bigint memory x = BigInt.fromUint(7);
bigint memory y = BigInt.fromUint(type(uint).max);
bigint memory z = x.add(y);
assert(z.limb(1) > 0);
}
}
라이브러리 타입을 address 타입으로 변환해, 즉 address(LibraryName)를 사용해 라이브러리의 주소를 얻는 것이 가능해. 컴파일러는 라이브러리가 배포될 주소를 모르므로, 컴파일된 16진 코드에는 __$30bbc0abd4d6364515865950d3e0d10953$__ 형태의 자리 표시자(형식은 <v0.5.0에서 달랐음)가 포함될 거야. 이 자리 표시자는 라이브러리의 완전히 정규화된 이름의 keccak256 해시의 16진 인코딩의 34문자 접두사예요. 예를 들어 라이브러리가 libraries/ 디렉터리의 bigint.sol라는 파일에 저장됐다면 libraries/bigint.sol:BigInt가 될 거야. 그러한 바이트코드는 불완전하며 배포해서는 안 돼. 자리 표시자를 실제 주소로 교체해야 해. 라이브러리가 컴파일될 때 컴파일러에 전달하거나, 링커를 사용해 이미 컴파일된 바이너리를 갱신해 그렇게 할 수 있어. 링킹에 명령줄 컴파일러를 사용하는 방법은 라이브러리 링킹을 보세요.
컨트랙트와 비교해 라이브러리는 다음 방식으로 제한돼:
- 상태 변수를 가질 수 없어.
- 상속받거나 상속될 수 없어.
- Ether를 받을 수 없어.
- 파괴될 수 없어.
(이것들은 추후에 완화될 수 있음.)
라이브러리의 함수 시그니처와 선택자 (Function Signatures and Selectors in Libraries)
public이나 external 라이브러리 함수에 대한 외부 호출이 가능하지만, 그러한 호출에 대한 호출 규약은 Solidity에 내부적인 것으로 간주되며 일반 컨트랙트 ABI의 지정과 같지 않아. 외부 라이브러리 함수는 재귀 구조체와 storage 포인터 같은 외부 컨트랙트 함수보다 더 많은 인자 타입을 지원해. 그 때문에 4바이트 선택자를 계산하는 데 사용되는 함수 시그니처는 내부 명명 스키마를 따르고, 컨트랙트 ABI에서 지원되지 않는 타입의 인자는 내부 인코딩을 사용해.
시그니처의 타입에 다음 식별자가 사용돼:
- 값 타입, non-storage
string, non-storagebytes는 컨트랙트 ABI와 같은 식별자를 사용. - non-storage 배열 타입은 컨트랙트 ABI와 같은 규약을 따름. 즉 동적 배열은
<type>[], M개 요소의 고정 크기 배열은<type>[M]. - non-storage 구조체는 완전히 정규화된 이름으로 참조됨. 즉
contract C { struct S { ... } }에 대해C.S. - storage 포인터 매핑은
mapping(<keyType> => <valueType>) storage를 사용. 여기서<keyType>과<valueType>은 각각 매핑의 키와 값 타입에 대한 식별자. - 다른 storage 포인터 타입은 해당 non-storage 타입의 타입 식별자를 사용하되, 단일 공백과
storage를 뒤에 붙임.
인자 인코딩은 가리키는 storage 슬롯을 참조하는 uint256 값으로 인코딩되는 storage 포인터를 제외하고 일반 컨트랙트 ABI와 같아. 컨트랙트 ABI와 유사하게 선택자는 시그니처의 Keccak256-해시의 첫 네 바이트로 구성돼. 그 값은 다음과 같이 .selector 멤버를 사용해 Solidity에서 얻을 수 있어:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.14 <0.9.0;
library L {
function f(uint256) external {}
}
contract C {
function g() public pure returns (bytes4) {
return L.f.selector;
}
}
라이브러리에 대한 호출 보호 (Call Protection For Libraries)
서론에서 언급했듯이, 라이브러리의 코드가 DELEGATECALL이나 CALLCODE 대신 CALL을 사용해 실행되면, view나 pure 함수가 호출되지 않는 한 되돌아가. EVM은 컨트랙트가 CALL로 호출됐는지 직접 감지하는 방법을 제공하지 않지만, 컨트랙트는 ADDRESS opcode를 사용해 현재 실행 중인 "위치"를 알아낼 수 있어. 생성된 코드는 이 주소를 생성 시점에 사용된 주소와 비교해 호출 모드를 결정해.
더 구체적으로 라이브러리의 런타임 코드는 항상 push 명령으로 시작하는데, 이것은 컴파일 시점에 20바이트의 0이야. 배포 코드가 실행되면 이 상수는 memory에서 현재 주소로 대체되고, 이 수정된 코드가 컨트랙트에 저장돼. 런타임에 이는 배포 시점 주소가 스택에 푸시되는 첫 번째 상수가 되고, 디스패처 코드는 non-view이거나 non-pure인 모든 함수에 대해 현재 주소를 이 상수와 비교해.
즉 체인에 저장된 라이브러리의 실제 코드는 컴파일러가 deployedBytecode로 보고하는 코드와 다르다는 뜻이야.
Using For
using A for B 지시문은 함수(A)를 사용자 정의 값 타입에 대한 연산자로, 또는 어떤 타입(B)에 대한 멤버 함수로 첨부하는 데 사용될 수 있어. 멤버 함수는 호출되는 객체를 첫 파라미터로 받아(Python의 self 변수처럼). 연산자 함수는 피연산자를 파라미터로 받아. 파일 수준이나 컨트랙트 안, 컨트랙트 수준에서 유효해.
첫 부분 A는 다음 중 하나일 수 있어:
- 함수 목록, 선택적으로 연산자 이름이 할당됨(예:
using {f, g as +, h, L.t} for uint). 연산자가 지정되지 않으면 함수는 라이브러리 함수나 자유 함수일 수 있고, 타입에 멤버 함수로 첨부돼. 그렇지 않으면 자유 함수여야 하고 타입에 대한 그 연산자의 정의가 돼. - 라이브러리의 이름(예:
using L for uint) - 라이브러리의 모든 non-private 함수가 타입에 멤버 함수로 첨부돼.
파일 수준에서 두 번째 부분 B는 명시적 타입이어야 해(데이터 위치 지정자 없이). 컨트랙트 안에서는 타입 대신 *를 사용할 수도 있어(예: using L for *;). 이는 라이브러리 L의 모든 함수가 모든 타입에 첨부되는 효과를 가져.
라이브러리를 지정하면 라이브러리의 모든 non-private 함수가 첨부되는데, 첫 파라미터의 타입이 객체의 타입과 일치하지 않는 것조차 포함돼. 타입은 함수가 호출되는 지점에서 검사되고 함수 오버로드 해석이 수행돼. 함수 목록을 사용하면(예: using {f, g, h, L.t} for uint), 타입(uint)이 이 함수 각각의 첫 파라미터로 암시적으로 변환될 수 있어야 해. 이 검사는 이 함수 중 어떤 것도 호출되지 않아도 수행돼. private 라이브러리 함수는 using for가 라이브러리 안에 있을 때만 지정될 수 있다는 점을 참고해.
연산자를 정의하면(예: using {f as +} for T), 타입(T)은 사용자 정의 값 타입이어야 하고 정의는 pure 함수여야 해. 연산자 정의는 전역이어야 해. 이렇게 정의할 수 있는 연산자는 다음과 같아:
| 분류 | 연산자 | 가능한 시그니처 |
|---|---|---|
| 비트(Bitwise) | & |
function (T, T) pure returns (T) |
| |
function (T, T) pure returns (T) |
|
^ |
function (T, T) pure returns (T) |
|
~ |
function (T) pure returns (T) |
|
| 산술(Arithmetic) | + |
function (T, T) pure returns (T) |
- |
function (T, T) pure returns (T) |
|
function (T) pure returns (T) |
||
* |
function (T, T) pure returns (T) |
|
/ |
function (T, T) pure returns (T) |
|
% |
function (T, T) pure returns (T) |
|
| 비교(Comparison) | == |
function (T, T) pure returns (bool) |
!= |
function (T, T) pure returns (bool) |
|
< |
function (T, T) pure returns (bool) |
|
<= |
function (T, T) pure returns (bool) |
|
> |
function (T, T) pure returns (bool) |
|
>= |
function (T, T) pure returns (bool) |
단항과 이항 -는 별도 정의가 필요하다는 점을 참고해. 컴파일러는 연산자가 어떻게 호출되는지에 따라 올바른 정의를 선택할 거야.
using A for B; 지시문은 현재 스코프(컨트랙트 또는 현재 모듈/소스 단위) 안에서만, 그 모든 함수 안을 포함해 활성이고, 사용된 컨트랙트나 모듈 밖에서는 효과가 없어. 지시문이 파일 수준에서 사용되고 같은 파일의 파일 수준에서 정의된 사용자 정의 타입에 적용되면, 끝에 global이라는 단어를 추가할 수 있어. 이는 함수와 연산자가 using 문장의 스코프뿐 아니라 타입이 사용 가능한 모든 곳(다른 파일 포함)에서 타입에 첨부되는 효과를 가져.
라이브러리 함수 대신 파일 수준 함수를 사용해 Libraries 섹션의 집합 예를 이렇게 다시 써보자.
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.13;
struct Data { mapping(uint => bool) flags; }
// Now we attach functions to the type.
// The attached functions can be used throughout the rest of the module.
// If you import the module, you have to
// repeat the using directive there, for example as
// import "flags.sol" as Flags;
// using {Flags.insert, Flags.remove, Flags.contains}
// for Flags.Data;
using {insert, remove, contains} for Data;
function insert(Data storage self, uint value)
returns (bool)
{
if (self.flags[value])
return false; // already there
self.flags[value] = true;
return true;
}
function remove(Data storage self, uint value)
returns (bool)
{
if (!self.flags[value])
return false; // not there
self.flags[value] = false;
return true;
}
function contains(Data storage self, uint value)
view
returns (bool)
{
return self.flags[value];
}
contract C {
Data knownValues;
function register(uint value) public {
// Here, all variables of type Data have
// corresponding member functions.
// The following function call is identical to
// `Set.insert(knownValues, value)`
require(knownValues.insert(value));
}
}
이런 방식으로 내장 타입을 확장하는 것도 가능해. 이 예에서는 라이브러리를 사용할 거야.
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.13;
library Search {
function indexOf(uint[] storage self, uint value)
public
view
returns (uint)
{
for (uint i = 0; i < self.length; i++)
if (self[i] == value) return i;
return type(uint).max;
}
}
using Search for uint[];
contract C {
uint[] data;
function append(uint value) public {
data.push(value);
}
function replace(uint from, uint to) public {
// This performs the library function call
uint index = data.indexOf(from);
if (index == type(uint).max)
data.push(to);
else
data[index] = to;
}
}
모든 외부 라이브러리 호출은 실제 EVM 함수 호출이라는 점을 참고해. 즉 memory나 값 타입을 전달하면 self 변수의 경우에도 복사가 수행돼. 복사가 수행되지 않는 유일한 상황은 storage 참조 변수가 사용되거나 내부 라이브러리 함수가 호출될 때야.
또 다른 예는 사용자 정의 타입에 대한 커스텀 연산자를 정의하는 방법을 보여줘:
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.19;
type UFixed16x2 is uint16;
using {
add as +,
div as /
} for UFixed16x2 global;
uint32 constant SCALE = 100;
function add(UFixed16x2 a, UFixed16x2 b) pure returns (UFixed16x2) {
return UFixed16x2.wrap(UFixed16x2.unwrap(a) + UFixed16x2.unwrap(b));
}
function div(UFixed16x2 a, UFixed16x2 b) pure returns (UFixed16x2) {
uint32 a32 = UFixed16x2.unwrap(a);
uint32 b32 = UFixed16x2.unwrap(b);
uint32 result32 = a32 * SCALE / b32;
require(result32 <= type(uint16).max, "Divide overflow");
return UFixed16x2.wrap(uint16(a32 * SCALE / b32));
}
contract Math {
function avg(UFixed16x2 a, UFixed16x2 b) public pure returns (UFixed16x2) {
return (a + b) / UFixed16x2.wrap(200);
}
}