당신이 블록체인 노드를 개발해야 된다. EVM을 직접 만들어야 돼.
상식적으로 생각하면 된다.
만약
상황을 가정하고, EVM의 동작 방식을 먼저 살펴보자.
{
"to": "0x1234567890abcdef1234567890abcdef12345678",
"from": "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd",
"gas": "0x5208",
"gasPrice": "0x3B9ACA00",
"value": "0x0",
"nonce": "0x1",
"data": "0x552410770000000000000000000000000000000000000000000000000000000000000064"
}
여기서 우리가 주목할 건 to, data, gas!
상태 DB에서 to인 주소를 찾아간다.
해당하는 컨트랙트의 값으로
가 있을 것이다. 이들을 통해 Code, Storage를 불러올 수 있다.
State DB와는 별도의, Code Storage에서 바이트 코드를 불러와야 한다.
CodeHash를 Key로, 해당 컨트랙트의 런타임 코드를 겟또!
컨트랙트 마다 존재하는, Storage Trie에서 StorageRootHash를 사용한다.
MPT구조이기 때문에, 단순히 조회가 안되고,
keccak256(slot 번호)를 사용해 변수의 위치를 찾아야 한다.
그리고 해당 슬롯에서 변수 값을 겟또!
오케이, 이젠 우린 코드와 전역변수 준비완료.
컨트랙트는 위와 같은 글로벌 객체에 접근할 수 있다는 걸 알 것이다.
EVM도 이를 대비해 트랜잭션으로 부터 msg,tx 객체를,
최신 블록을 불러와 block을 가져온다.
이건 뭐 별거 없다.
이러면 준비가 끝난 것이다.
코드를 실행하기 위한 사전 준비, 즉 실행 컨텍스트를 생성하는 과정이다.
이러면 트랜잭션 실행을 위한 EVM 세팅(인스턴스 생성)이 완료되었다.
존나 중요한 점은
라는 점이다. 한 트랜잭션을 위해서 새로 위와 같은 세팅을 한다는 것을 기억하자.
문제 발생시 false를 반환하는, 동일한 OPCODE와 대응하는,
컨트랙트 간 호출을 수동으로 구성할 수 있는 저수준 함수다.
contract Hi is Insa {
constructor(address _faucet) {
_faucet.call("withdraw", 0.1ether)
}
new, 인터페이스, 컨트랙트 파일 가져와서 호출할 수도 있지만,
이렇게 무식하게 컨트랙트 주소만을 가지고 호출도 가능한 것을 알 것이다.
중요한 건 그게 아니다.
내가 아까 뭐랬는가? 트랜잭션 마다, EVM 인스턴스가 새로 생성된다고 했다.
만약 내가 트랜잭션 A를 실행했다면, 위와 같이 실행 컨텍스트를 생성해 EVM이 세팅된다.
만약 내가 일으킨 트랜잭션이, 다른 함수를 호출하는 call이 포함되었다면,
새로운 EVM 인스턴스가 내부적으로 생성된다는 것이다.
그러면 이런 의문이 들어야한다.
"그럼 내부적으로 실행컨텍스트 생성할 때, 위처럼 msg 객체, ,gas는 어디서 받아와야하나?"
외부 EVM에서 상속을 받거나, 정보를 바탕으로 다시 세팅된다.
이렇게 새로 EVM 인스턴스가 생성되는 것도 중요하지만, 더 중요한 것은,
이것을 이용한 보안문제가 "재진입성 문제"다.
컨트랙트 A에서 함수 a가
순서대로 작성되었다면,
컨트랙트 B가
을 해놓으면, 잔액 업데이트는 실행되지 않고, 외부 EVM은 이더전송에 멈춰잇고,
내부 EVM에선 계속 돈 받으면서 무한 a 실행을 할 것이다.
내부적으로 계속 evm이 생성되니까 잔액 업데이트를 할 수가 없는 것이다.
몬느낌인지 알지?
자 정리,
이에 따라
delegatecall = 위임하여 호출, 대리 호출.
이제 감이 오는가? 본래 외부 컨트랙트를 호출하려면,
패시브가 새로운 EVM을 생성해야 된다.
가 원칙이다.
그런데 위임한다? 대리한다?
정말 지리는 완벽한 빌드업이다.
이런식으로 가르쳐야지 무식하게 뭔 외우라고 하면 이해가 되겠냐? 제가 책 쓸게요 ㄹㅇ
거의 다 왔다. 주의할 점이 뭐가 있을까?
다른 컨트랙트를 호출하는데, EVM 세팅은 그대로라면?
contract B {
uint256 public num; // 🔹 storage slot #0
}
contract A {
string public hi; // 🔹 storage slot #0 (이전과 다른 타입)
function delegatecallSetNum(address _contractB, uint256 _num) public {
(bool success, ) = _contractB.delegatecall(
abi.encodeWithSignature("setNum(uint256)", _num)
);
require(success, "Delegatecall failed");
}
}
컨트랙트 A를 실행하고 있었다. delegatecall로 B의 변수를 변경하려고 한다.
EVM 실행 컨텍스트의 Storage에는 뭐가 세팅되어있는가? A의 스토리지 슬롯들만 있다.
그러면 실제로 변경되는건 B가 아니라? A의 슬롯 데이터다.
이걸 주의해야 한다.
msg.sender는 바뀌나? 그대로. tx?block? 다 그대로.
라이브러리 호출을 생각해보자.
라이브러리 객체는 전역변수가 없다. 오로지 함수만 있는데,
라이브러리를 호출할 때도 내부적으로 delegatecall을 사용한다.