컨트랙트가 배포되면, 코드는 수정할 수 없다.
여기서 말하는 Code는 전체 솔리디티 코드에 해당하는 부분이 아닌, "함수부" 이다.
변수야 함수 로직을 통해서 수정할 수 있으니까 제외하고,
함수의 로직은 배포되면 수정할 수 없음을 기억하자.
그런데 아무리 불변성이니, 무결성이니 해도,
정말 이러한 특성을 지닌 코드들을 사용자 관점에서 사용하기 좋을까?
컨트랙트도 이더를 소유할 수 있다.
개발자가 컨트랙트를 업그레이드하고 싶다. 새로운 기능을 추가하고 싶다.
그럼 사용자에게 일일히 "새로운 컨트랙트 주소는 이거구요, 님이 넣으셨던 돈은 돌려드렸고~"
기존 컨트랙트의 데이터와 잔액은 일일히 따로 빼고, 새로운 컨트랙트에 넣고.....
정말 귀찮다. 너무너무 불편하다.
프록시는 많이 들어봤을 것이다.
이란 뜻으로, 서버 개발에서도 많이 등장하는 개념이다.
아래 그림을 보자.
이 그림이 Upgradable contract의 기초이자 핵심이다.
사용자는 아무고토 모르고 그냥 컨트랙트에 있는 함수 setValue()를 호출한다.
근데 사실 그건 proxy 컨트랙트이고, 여기서 delegatecall을 사용해 실제 로직이 있는 컨트랙트에
setValue()를 호출한다.
뭔가 알듯말듯하다고? 찬찬히 자세히 보자.
proxy에 실제 Logic이 존재하는 컨트랙트의 주소를 저장해야 한다.
그래야 그 주소로 delegatecall을 날릴 것 아닌가?
그리고, 새로운 컨트랙트를 만들었을 때, 새로운 주소로 쏴주기 위해선 변수를 업데이트해야 하지 않는가?
이를 위한 변수이며, 수정할 수 있어야 한다.
솔리디티에서 fallback() 함수에 대해 아는가?
사용자가 코드에 없는 함수를 호출하려고 할때 실행이 되는 함수다.
LogicV1과 Proxy는 함께 개발하고 배포할 것이다.
이 시점에는 Proxy가, LogicV1이 어떤 함수들이 선언되어 있는지 알 수 있다.
그런데, LogicV2에서도 새로운 함수를 추가하려고 한다.
Proxy가 이들을 알 수 있는가? 아니면 LogicV1에 있는 함수들만 만들고, 내용만 수정해야 하나?
너무나 불편하고 한정적이지 않은가?
이를 위해 Fallback()을 사용하는 것이다.
위 그림을 다시보자. V1에서 setValue()가 있다.
사용자는 아무고토 모르고 SetValue()를 Proxy에 날린다.
Proxy에는 이 함수가 없다. 그러면 fallback()을 실행시키고, 그 안에서
V1주소로 setValue()를 실행하는 것이다.
어떻게 ? msg.data에 함수 식별자와 파라미터를 넣어서.
(트랜잭션 데이터 구조는 이전 포스팅에 자세히 나와있다.)
이처럼 fallback과 msg.data를 엮어서,
어떤 함수든 구애받지 않고 V1,V2는 만들 수 있고, 사용자들도 자유롭게 호출할 수 있는 것이다.
참 쉽죠?
delegatecall과 call의 차이는?
delegatecall은 기존 EVM 인스턴스를 그대로 사용해서, 스토리지 슬롯 저장 문제가 발생할 수 있고,
msg 등 글로벌 객체가 고대로 들어간다.
즉 사용자 -> proxy -> V1 으로 delegatecall을 하면,
proxy -> v1 에서 msg.sender는 proxy가 아니라 사용자다. 가스비도 사용자가 낸다.
느낌이 오는가?
delegatecall을 사용했기에 사용자는 직접 V1에 함수를 호출하는 줄 아는 것이다.
아주아주 쉬운 예제로 실습을 해보자.
고객 포인트 계산하는 로직을 변경하고 싶다고 생각해보자.
V1은 value를 넣으면 고대로 저장한다.
근데 V2에서는 value를 넣으면 x2를 해서 저장하고 싶은 것이다.
여기에 V2에서는 새로운 함수도 추가하고 싶은 것이다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Proxy {
address public logic;
address public admin;
constructor(address _logic) {
logic = _logic;
admin = msg.sender;
}
function setLogic(address _newLogic) external {
require(msg.sender == admin, "Not admin");
logic = _newLogic;
}
fallback() external payable {
(bool success, bytes memory result) = logic.delegatecall(msg.data);
require(success, "Delegatecall failed");
}
// 확인용 getter
function getValue() public view returns (uint256 val) {
assembly {
val := sload(0)
}
}
}
핵심은 역시 fallback 부분이다.
fallback() external payable {
(bool success, bytes memory result) = logic.delegatecall(msg.data);
require(success, "Delegatecall failed");
}
변수로 넣은 V1 혹은 V2 주소로 delegatecall을 날리는데,
어떤 함수를 실행할지는 msg.data, 즉 사용자에게 맡기는 것이다.
msg.data를 활용했기에 하드코딩없이 어떤 함수든 새로이 만들수 있는 것이다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract LogicV1 {
uint256 public value;
function setValue(uint256 _value) public {
value = _value;
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract LogicV2 {
uint256 public value;
function setValue(uint256 _value) public {
value = _value * 2;
}
function newLogic(uint256 number) public pure returns(uint256) {
return number * 10;
}
}
처음엔 proxy 상태가 위와 같다. 이제 msg.data에, setValue(42)를 실행할 것이다. remix에는 msg.data를 위한 필드를 만들어줬다.
setvalue(uint256) 을 해싱하고 4바이트, 그리고 뒤에 파라미터 42를 16진수로 나타내니
0x55241077
000000000000000000000000000000000000000000000000000000000000002a
가 나왔다. 이를 넣어보자.
getValue()를 통해 proxy의 slot 0을 조회하니 42가 나왔다.
이번엔 V2를 배포하고, 주소를 바꿔서 다시 setValue(42)를 실행해보자.
84가 나왔다. 로직이 수정된 것을 반영할 수 있다. 끝~
뭔가 찝찝한 느낌이 들어야 한다.
그렇다. 나는 분명 V1, V2에 있는 변수인 value를 수정하려고 함수를 실행했는데,
왜 proxy에 있는 slot0이 42,84로 바뀐거지?
실제로 V1,V2의 value는 둘다 0이다.
그렇지, delegatecall은 기존 컨트랙트의 EVM 인스턴스를 그대로 쓴다.
트랜잭션 마다 새로운 EVM 인스턴스가 세팅된다고 했다.
위 그림을 보자. proxy 컨트랙트를 위해 storage, code가 세팅되고,
msg는 저 트랜잭션을 가리킨다. msg.sender = 사용자, msg.data = setValue(42)
이해 되지?
이제 fallback이 실행되서 delegatecall을 실행한다.
내부적으로 새로운 EVM 인스턴스가 만들어지냐?
아니라고, 있는거 그대로 쓴다고. 코드만 갖다가 쓰는거라고.
code에 V1 코드만 가져와서 쭉 읽으면서 실행을 한다.
setValue가 뭐하는 코드였지?
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract LogicV1 {
uint256 public value;
function setValue(uint256 _value) public {
value = _value;
}
}
value를 42로 바꾸는 것이였다.
위 그림을 봐라. 해당 컨트랙트의 value는 slot0에 저장되어 있을 것이다.
EVM도 이를 알고 storage에 가서 slot0 값을 42로 바꾼다.
위 그림 다시봐라. storage에 어떤 데이터가 있는가? proxy contract가 있다.
Proxy에 slot0이 뭐였었지?
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Proxy {
address public logic;
address public admin;
constructor(address _logic) {
logic = _logic;
admin = msg.sender;
}
logic, 컨트랙트 주소다. 이게 42로 수정된다.
logic의 값을 살펴보니 84로 바뀌어 있다.
머리로는 이해했어.
근데 나는 V1,V2의 값을 바꾸려고 함수를 실행한건데, Proxy가 바뀌면 뭘 어쩌자는거임?
라는 생각이 들어야 한다.
이를 위한 것은 심화판에서 보자. 계속...