part 2 에서는 유저와 어드민의 인터페이스를 나누었고 이번에는 프록시의 관리를
위해 ProxyAdmin 이라는 컨트랙트를 만들어 관리를 한다.
왜 나누어야 하는지?
아래와 같이 프록시, 로직 컨트랙트에는 같은 이름을 가진 함수가 있다.
- Proxy
function admin() external checkAdmin returns(address) {
return _getAdmin();
}
function implementation() external checkAdmin returns(address) {
return _getImplementation();
}
- CounterV1
function admin() external pure returns(address) {
return address(1);
}
function implementation() external pure returns(address) {
return address(2);
}
그리고 이 함수는 아래의 checkAdmin modifier 를 통해 유저와 어드민 인터페이스로 나누어진다.
modifier checkAdmin() {
if(msg.sender == _getAdmin()){
_;
} else {
_fallback();
}
}
어드민 이라면 Proxy 컨트랙트 내부의 함수를 호출,
유저라면 CounterV1 인 로직 컨트랙트의 함수를 호출.
따라서, 어드민이 로직 컨트랙트의 함수를 호출할 수 있는 방법이 없다.
그래서 ProxyAdmin 컨트랙트를 생성하여admin(나) => ProxyAdmin => Proxy => CounterV1~
의 순서대로 컨트랙트를 제어하고 어드민이 로직 컨트랙트 함수를 호출 할 수 있도록 코드를 수정한다.
먼저 Proxy 컨트랙트에 함수를 추가한다. 현재 Proxy 컨트랙트 내에는 admin 을 수정하는 함수가 있지만, 생성자에 의해 컨트랙트가 만들어질때에만 사용된다.
따라서, 새로 만드는 ProxyAdmin 컨트랙트로 admin 을 변경해주어야 하기에 아래의 함수를 추가한다.
function changeAdmin(address _admin) external checkAdmin {
_setAdmin(_admin);
}
아래와 같이 필수적인 코드를 적어준다. admin 으로 설정할 주소를 상태변수로 선언하고 생성자로 배포한 주소를 admin 으로 넣어준다.
그리고 onlyOwner modifier 를 만들어준다.
contract ProxyAdmin {
address public admin;
constructor() {
admin = msg.sender;
}
modifier onlyOwner() {
require(msg.sender == admin, "Admin only.");
_;
}
}
그리고 현재 ProxyAdmin 컨트랙트의 admin 을 변경하는 함수도 만들고, 로직 컨트랙트을 업그레이드 하는 함수도 만들어준다.
function changeProxyAdmin(address payable _Proxy, address _admin) external onlyOwner {
Proxy(_Proxy).changeAdmin(_admin);
}
function upgrade(address payable _Proxy, address _implementation) external onlyOwner {
Proxy(_Proxy).upgradeTo(_implementation);
}
또한, 현재 Proxy 컨트랙트의 어드민 주소와 로직 컨트랙트 주소를 반환받는 함수를 구현한다.
function getProxyAdmin(address _Proxy) external view returns(address) {
(bool success, bytes memory res) = _Proxy.staticcall(abi.encodeCall(Proxy.admin, ()));
require(success, "Failed to call.");
return abi.decode(res, (address));
}
function getProxyImplementation(address _Proxy) external view returns(address) {
(bool success, bytes memory res) = _Proxy.staticcall(abi.encodeCall(Proxy.implementation, ()));
require(success, "Failed to call.");
return abi.decode(res, (address));
}
이때 사용되는 것은 staticcall 인데, 이것은 상태변수를 변경시키지 않는 call 이다. 왜 이것을 사용하는지 Proxy 컨트랙트의 함수를 확인해보자.
- Proxy
function admin() external checkAdmin returns(address) {
return _getAdmin();
}
function implementation() external checkAdmin returns(address) {
return _getImplementation();
}
modifier checkAdmin() {
if(msg.sender == _getAdmin()){
_;
} else {
_fallback();
}
}
어떠한 상태변수도 수정하지 않기 때문에 두 함수는 사실상 view 함수로 볼 수 있다.
하지만, 두 함수에 사용하는 checkAdmin modifier 내의 else 문에서 fallback 함수를 호출할 수도 있기에 view 설정이 불가능하다.
따라서, view 함수가 없으므로 상태변수를 수정하지 않는 staticcall 로 호출하는 것이다. 이렇게 해야 불필요한 가스의 소모 없이 함수를 호출할 수 있다.
위에서 설명한대로 어드민은 위와 ProxyAdmin 컨트랙트를 통해 전체 컨트랙트를 제어하게 되며, 어드민도 유저와 같이 로직 컨트랙트 함수를 호출할 수 있다. 직접 배포하고 확인해보았다.
컨트랙트 4개를 모두 배포한다.
Proxy 컨트랙트의 admin 을 ProxyAdmin 주소로 바꾼다.
ProxyAdmin 컨트랙트에서 CounterV1, Proxy 컨트랙트 주소를 인자로 넣고 업그레이드 해준다.
배포할 컨트랙트로 CounterV1 을 설정한 후, At Address 에 Proxy 컨트랙트 주소를 넣고 클릭.
함수를 호출하는 주소가 admin 임에도 유저와 같은 결과 값을 반환한다. 성공.
increase 함수로 count 상태변수를 5로 놓고 CounterV2 로 업그레이드.
CounterV2 로 업그레이드 했고, 상태변수 count 는 5 인 상태로 업그레이드까지 끝.
오픈제플린의 프록시 라이브러리의 컨트랙트 코드를 뒤져보았지만, 사실 이해가 잘 되질 않았다.
하지만 직접 컨트랙트를 만들고 배포해보니 이런 방법으로 전체 컨트랙트가 관리되는구나.. 하고 알게되었다.
특히 잘 사용하지 않는 어셈블리, staticcall, abi.encodeCall, abi.decode 등을 사용해서 컨트랙트를 작성한게 좋은 경험이 된 것 같다.
작성한 코드는 여기에.. Proxy contract 실습 코드