Cross-contract and Blockchain Interaction
EVM Contract Function Execution
과거 블록들 (Block n-2, Block n-1):
과거 블록은 이미 채굴되었으며, 스마트 컨트랙트와 관련된 트랜잭션(TX)을 포함한다.
스마트 컨트랙트 코드 또는 상태 변경이 이 블록들에 기록된다.
현재 블록 (Block n):
아직 채굴 중인 블록이며, 실행될 트랜잭션을 포함한다
EVM에서의 실행
EVM은 다음 정보를 기반으로 실행된다:
- Coinbase: 블록 보상을 받을 주소.
- Difficulty: 현재 블록을 채굴하는 데 필요한 난이도.
- Gas Limit: 블록 내 최대 가스 소비 한도.
- Timestamp: 블록 생성 시점의 시간.
상태 변경:
트랜잭션 실행 결과로 컨트랙트 상태가 변경된다.
이러한 변경 사항은 Write Set으로 기록된다.
결과 블록 생성
Write Set을 통해 변경된 상태는 새 블록(Block n+1)에 기록된다.
새 블록에는 트랜잭션(TX)과 함께 다음 데이터가 포함됩니다:
Coinbase, Difficulty, Gas Limit, Timestamp 등.
이 블록은 채굴 후 블록체인에 추가된다.
Transactions and Messages
EOA (Externally Owned Account)
스마트 컨트랙트 함수 호출의 시작이다.
외부 소유 계정(EOA)이 트랜잭션을 통해 스마트 컨트랙트를 호출한다.
Contract Function Call
모든 스마트 컨트랙트 함수 호출은 트랜잭션으로 시작된다.
트랜잭션은 EOA에서 스마트 컨트랙트로 전송되며, 가스 비용이 포함되다.
다단계 컨트랙트 호출
복잡한 시스템은 여러 스마트 컨트랙트가 서로 호출될 수 있다.
예를 들어, A 컨트랙트가 B 컨트랙트를 호출하고, B 컨트랙트가 다시 C 컨트랙트를 호출한다.
이런 경우, 최초 트랜잭션의 가스가 중간 호출(B, C)에 전달되어야 한다.
컨트랙트가 다른 컨트랙트에 메시지를 발행할 때마다 원본 트랜잭션의 가스가 그냥 전달된다.
그러나 때때로 이것은 의도되지 않은 경우(예: Ether만 전송해야 하는 경우)다.
따라서 Solidity 주소 클래스를 사용해 해당 사용 사례에 특별히 적합한 함수를 구현한다
Address Class
몇몇의 컨트랙트들은 특정 계좌나 계좌 잔액같은 정보들을 요구할 수 있다. Solidity는 address라는 특별하느 타입의 요소를 사용한다. 이더리움 계좌에서 EOA나 컨트랙트는 address 객체로 표현할 수 있다.(class함수 같은것)
이더리움을 받는 address는 20바이트의 16진수 코드로 표현한다,
address a = 0xd5e7726990fD197005Aae8b3f973e7f2A65b4c18
Ether를 받을 수 있는 address는 address payable로 정의하거나 Ether를 전송하는 동안 payable(<address>) 함수로 선언야 한다.
형변환
더 나아가 컨트랙트 객체는 아래 코드처럼 address에 명시적으로 형변환(casting)을 할 수 있다.
형변환: ex)모든 새는 동물이다. 따라서 모든 새는 동물이라고 부를 수 있다.
contract A {
function f() {}
}
contract B {
function g() {
A a= newA();
addresscontract_a = address(a);
address self = address(this);
}
}
down-cast(부모클래스가 특정 자식클래스 특성을 가진다)도 가능하다:
Aa = A(0xd5e7726990fD197005Aae8b3f973e7f2A65b4c18)
당연히 이 다운캐스팅은 주소에 의해 A의 특성이 식별된 경우에만 가능하다.(모든 동물은 모두 새가 아니다. 즉 어떤 동물인지 표시해야함 + 모든 고양이와 모든 새는 동물이지만 고양이보고 동물이라 한뒤 저건 동물이니 새라고 할 수 없다.)
Class 함수
1. <address>.balance
특정 주소의 잔액을 256비트의 정해지지않은 형식의 Wei 단위로 반환한다.
Wei는 이더리움의 가장 작은 단위이다.
1 Ether = 10^18 Wei
2. <address>.transfer(uint256 value)
지정된 Wei 금액을 특정 주소로 전송한다. 실패 시 예외를 발생시킨다.
2300 gas만 전송되므로, 호출된 스마트 컨트랙트가 복잡한 작업을 수행하려 하면 실패할 가능성이 있다.
3. <address>.send(uint256 value)
transfer와 비슷하지만, 실패 시 false를 반환한다.
마찬가지로 2300 gas만 전송된다.
4. <address>.call(...)
함수 호출 및 Ether 전송에 사용할 수 있는 저수준 함수이다.
기본적으로 모든 가스(gas)를 전달하므로, 호출된 컨트랙트가 많은 가스를 소모할 수 있다.
이는 호출자에게 비용이 증가하는 원인이 될 수 있다.
fallback 함수 또는 receive 함수가 정의되지 않은 경우, 2300 gas만 전달된다.
5. <address>.delegatecall(...)
호출된 컨트랙트의 상태를 호출자의 컨텍스트에 위임하여 실행하는 저수준 함수이다.
주의: 호출자의 저장소와 컨텍스트를 사용하므로 보안 문제의 원인이 될 수 있다.
실패 시 false를 반환한다.
호출 컨트랙트가 신뢰할 수 있는 경우에만 사용해야 한다.
Re-entrancy attack
Re-entrancy Attack은 Solidity 스마트 컨트랙트에서 발생할 수 있는 가장 심각한 공격 중 하나이다.
스마트 컨트랙트에서 외부의 신뢰할 수 없는 다른 컨트랙트를 호출할 때 발생한다.
공격자는 재귀 호출(recursive calls)을 통해 호출된 함수를 반복적으로 실행한다.
이 과정에서 원래 함수가 잔고(balance)를 갱신하기 전에 재귀 호출이 이루어지며, 반복적으로 자금을 인출하여 스마트 컨트랙트의 자금을 고갈시킨다.
기존 함수가 코인을 전송한 이후에 받는 컨트랙트의 잔고를 갱신하면 성공하게 된다.
즉 공격으로 재귀호출 >> 기존함수가 잔고 갱신 >> 다시 재귀호출.... 반복
재진입 공격(Re-entrancy Attack) 시나리오
A 계약(Contract A): 이더리움(ETH)을 예치하고 인출할 수 있는 기능을 제공하며, 다른 계약들에 대한 부채를 기록한다.
B 계약(Contract B): A 계약의 withdraw() 함수를 반복적으로 호출해 악용하는 fallback() 및 attack() 함수를 보유한다.
아래 경우에는 A가 B에게 1ETH를 갚아야 한다.
- B 계약이 attack() 함수를 호출한다.
attack()은 A 계약의 withdraw()를 호출하며 공격을 시작한다. - A 계약은 B 계약의 부채(debt > 0)가 0보다 큰지 확인한 후, 1 ETH를 B 계약으로 전송한다.
이 과정에서 A 계약의 잔액이 1 ETH 줄어들고, B 계약은 1 ETH를 받게 된다.
부채(debt)는 그대로 유지된다. - B 계약이 이더를 받을 때마다, fallback() 함수가 자동으로 호출된다.
fallback() 함수는 즉시 A 계약의 withdraw()를 다시 호출한다. - 매번 withdraw() 함수가 호출될 때마다, A는 B로 1 ETH를 전송한다.
하지만 A 계약의 부채(debt > 0)는 아직 갱신되지 않았기 때문에, A는 계속해서 ETH를 보내게 된다.
결과적으로 A의 잔액은 점점 줄어들고, B는 매번 1 ETH씩 더 받게 된다. - A 계약은 withdraw() 함수가 끝나야 부채를 0으로 초기화한다.
하지만 B 계약의 반복 호출로 인해 초기화 코드에 도달하지 못하게 된다. - 이 과정을 반복하며 A 계약의 잔액이 완전히 소진될 때까지 공격이 계속된다.
재진입 공격 방지 방법
상태 변경을 외부 계약 호출 전에 완료하기
외부 코드를 호출하기 전에 상태(예: 잔액 또는 내부 변수)를 먼저 업데이트하거나 변경한다.
이렇게 하면 외부 호출 중 재진입 시 상태를 다시 악용할 수 없다.
재진입 방지용 함수 수정자(Function Modifier) 사용
Solidity에서 modifier를 사용해 재진입 방지 메커니즘을 구현할 수 있다.
아래 예시는 ReEntrancyGuard라는 계약을 생성하여 locked라는 변수를 활용한다.
noReentrant 수정자는 함수 호출 시 재진입을 차단한다.
pragma solidity ^0.8.10;
contract ReEntrancyGuard {
bool internal locked;
modifier noReentrant() {
require(!locked, "No re-entrancy"); // 재진입이 발생하면 에러 반환
locked = true; // 함수 실행 시 잠금 상태 활성화
_; // 함수 본문 실행
locked = false; // 함수 종료 후 잠금 해제
}
}
객체: Message
Solidity에서는 함수 호출자와 관련된 정보를 얻기 위해 msg 객체를 제공한다.
이 객체는 호출자의 정보를 포함하며, 이는 계약 또는 외부 계정(Externally Owned Account)일 수 있다.
- msg.sender
함수 호출자의 계정 주소를 반환한다.
유형: address
(transfer, send, call 함수 호출 시 address payable로 캐스팅이 필요하다.) - msg.data
메시지 또는 트랜잭션의 전체 페이로드(payload) 를 반환한다.
트랜잭션 호출에 포함된 데이터를 확인할 때 사용된다. - msg.sig
호출된 함수의 해시 서명(signature) 를 반환한다.
이를 통해 EVM(Ethereum Virtual Machine)은 호출된 함수가 무엇인지 인식한다. - msg.value
메시지와 함께 전송된 Wei의 양을 반환한다.
Wei는 Ethereum의 최소 단위(1 Ether = 10¹⁸ Wei)이다.
메시지 객체는 항상 마지막 발신자를 참조하기 때문에 컨트랙트에서 이와 함께 사용할 때는 몇 가지 특별한 주의가 필요하다.
contract A {
function f() public returns () {
return msg.sender;
}
function g() public returns () {
return f(); // f() is called directly, msg.sender will be the address that calls g()
// f() does not need to be public as this is an internal call
}
function h() public returns () {
return this.f(); // f() is called by the current contract instance => msg.sender will always be equal to address(this) which is the contract address
// f() has to be public as this.f() behaves like an external call
}
}
객체: 블록
Solidity에서는 최신 블록에 대한 정보를 얻기 위해 block 객체라는 전역 변수를 제공한다.
주로 특정 함수의 실행을 시간적으로 제한하거나 블록 관련 데이터를 참조할 때 사용된다.
- block.coinbase
현재 블록을 제안한 프로포저(proposer)의 계정 주소를 반환한다.
블록 채굴 보상을 받을 수 있는 주소이다. - block.difficulty
현재 블록의 채굴 난이도를 나타낸다.
병합(Merge) 이후 이 값은 항상 0으로 반환된다. - block.gaslimit
현재 블록의 가스 한도(GASLIMIT) 를 반환한다.
블록에 포함될 수 있는 최대 가스 양을 나타낸다. - block.timestamp
현재 블록의 UNIX 타임스탬프를 반환한다.
이 값은 블록 프로포저가 조작할 가능성이 있으므로 절대적인 신뢰는 어렵다.
객체: 트랜잭션
트랜잭션 객체(tx)는 전역 객체로, msg 객체와 유사하지만 트랜잭션 자체에 대한 정보를 제공한다.
트랜잭션을 생성한 외부 계정(Externally Owned Account, EOA)에 대한 정보를 주로 다룬다.
- tx.origin
트랜잭션을 발행한 최초의 계정 주소를 나타낸다.
이 값은 항상 외부 소유 계정(EOA)를 가리킨다.
주의: 인증 목적으로 사용하지 말아야 한다. (phishing 공격 가능성 때문) - tx.gasprice
트랜잭션 발행자가 설정한 가스 가격(gas price) 정보다.
해당 값은 가스 한도와 실제 사용된 가스에 따라 트랜잭션 비용 계산에 사용된다.
Transaction Calldata
이더리움 트랜잭션 객체에는 추가 정보를 저장하는 data 필드가 포함되어 있다.
이 필드는 트랜잭션 수행에 필요한 데이터를 포함하며, Keccak 해시로 표현된 내용을 전달한다.
데이터 필드의 역할
data 필드는 다음 세 가지 중 하나를 포함할 수 있다:
- 스마트 컨트랙트의 바이트코드(bytecode):
컨트랙트 배포 시, 컨트랙트의 바이트코드가 이 필드에 포함된다. - 함수 시그니처와 인자(arguments):
스마트 컨트랙트의 특정 함수를 호출할 때, 호출할 함수의 시그니처와 전달할 인자 값이 포함된다. - 비어 있는 경우:
data 필드가 비어 있다면, 이는 단순히 두 계정 간의 이더 전송 트랜잭션임을 나타낸다.
트랜잭션 data 필드는 일반적으로 Calldata라고 불린다.
Calldata는 스마트 컨트랙트 함수 호출과 관련된 정보를 포함하고 있다.
Decoding the Calldata
Calldata는 스마트 컨트랙트 호출 시 사용되는 데이터이다. 이 데이터는 함수와 입력값을 포함하며, Keccak 해시를 이용해 인코딩된다. 디코딩 과정을 통해 호출된 함수와 전달된 인자를 알아낼 수 있다.
1. 처음 8개의 16진수 문자 (4 바이트)는 함수 시그니처를 나타낸다.
Keccak 해시를 사용하여 함수 이름과 매개변수를 특정할 수 있다.
예: transfer(address, uint256) → a9059cbb
2. 나머지 데이터는 인자 값을 64개의 16진수 문자로 표현한다.
- 첫 번째 인자: 주소 (address)
기본 길이는 40개의 16진수 문자이며, 앞의 24문자는 패딩이다.
예: 000000000000000000000000199af693595e27ac16772e07d17b178f222518b → 0x199af693595e27ac16772e07d17b178f222518b - 두 번째 인자: 256비트 부호 없는 정수 (uint256)
64개의 16진수 문자로 표현되며, 앞의 56문자는 패딩입니다.
예: 000000000000000000000000000000000000000000000000000000002d0062c0
→ 2d0062c0 (16진수 → 10진수 변환: 7550000) - 예시 디코딩 결과
함수 호출: transfer(address _to, uint256 _value)
전달된 데이터:
_to: 0x199af693595e27ac16772e07d17b178f222518b
_value: 7550000
Calldata는 ABI(Application Binary Interface)와 함께 사용되어 정확한 디코딩이 가능하며, 스마트 컨트랙트 호출의 핵심 데이터 구조이다.
Event(신호)
이벤트는 스마트 컨트랙트가 외부 애플리케이션(예: dApp)과 통신하기 위해 사용되는 신호(Events)이다.
이벤트는 스마트 컨트랙트에서 발생하며, 이를 통해 외부 애플리케이션은 컨트랙트 내부에서 발생한 특정 상황을 감지할 수 있다.
주요 특징
- 이벤트 정의 및 발행: 개발자는 특정 시점에 이벤트를 발생시키도록 결정할 수 있다.
예: 특정 함수 호출 시 이벤트 발행. - 외부 애플리케이션과의 상호작용:
dApp이나 이더리움 API를 사용하는 애플리케이션이 컨트랙트에서 발생한 이벤트를 감지할 수 있다. - 데이터 분석에도 활용 가능.
주요 활용 사례
- 사용자 인터페이스에 데이터 제공:
스마트 컨트랙트의 상태 변경을 UI에 실시간으로 반영. - 오라클 업데이트 트리거:
외부 데이터 소스(오라클)와 상호작용을 위한 신호 발행. - 저렴한 데이터 저장소(logging):
이벤트는 이더리움 블록체인에 기록되며, 이를 로깅 목적으로 사용 가능. 이는 데이터 저장에 소요되는 가스 비용을 줄일 수 있음.
Transfer Event
이더리움 블록체인에서 가장 일반적으로 사용되는 이벤트 중 하나는 Transfer 이벤트이다.
이는 토큰을 전송할 때 발생하며, 토큰 전송과 관련된 정보를 기록한다.
event Transfer(address indexed from, address indexed to, uint256 value);
from: 토큰을 보낸 주소
to: 토큰을 받은 주소
value: 전송된 토큰의 양
이벤트 로그 기록:
Transfer 이벤트는 누가 전송했는지(from), 누가 받았는지(to), 얼마의 양이 전송되었는지(value)를 블록체인에 기록한다.
indexed 키워드:
이벤트 속성에 indexed를 선언하면 특정 속성값을 기준으로 이벤트 로그를 효율적으로 필터링할 수 있다.
예: 특정 주소에서 발생한 모든 Transfer 이벤트 검색.
사용 사례
토큰 전송 추적:
예: 특정 주소(0xa0b52ef1...)에서 발생한 Transfer 이벤트를 필터링하여 검색.
블록체인 데이터 분석:
Transfer 이벤트를 통해 네트워크 상의 토큰 흐름을 분석 가능.
실시간 이벤트 로그 예시
Keccak 해시: 이벤트 이름과 시그니처를 해싱한 결과를 로그에 포함.
인덱싱된 파라미터: from, to 주소 값.
기타 파라미터: 전송된 토큰의 양(value).