dev/Solidity

interact with existing deployed contracts, raw calls

_April 2022. 6. 16. 19:30

외부 컨트랙트를 부를때

 

이렇게 인터페이스로 부르는 방식이 있고

//1
interface IVault {
    function createVault(uint256 gameId) external payable;
    function addAmount(uint256 gameId) external payable;
    function withdraw(uint256 gameId, address payable winner) external payable;
    function claim(uint256 gameId, address payable user) external payable;
}
//2
address vaultAddr;
//3
    function setVault(address _vaultAddr) external {
        vaultAddr = _vaultAddr;
    }
//4
...
	IVault(vaultAddr).addAmount{value: msg.value}(gameId);
...

 

ABI 로 부르는 방식이 있다.

import "./Vault.sol"; //외부컨트랙트 abi 불러옴

address vaultContract;

function setVault(address vaultAddr) external {
        vaultContract = vaultAddr;
    }
    
    ...
    
    (bool success, ) = vaultContract.call{value: msg.value}(
            abi.encodeWithSignature("createVault(uint256)", gameId)
        );
        require(success, "Failed to createVault vault");
        
    ...

 

나는 인터페이스 작성하기가 귀찮아서ㅠ ABI 로 불렀는데..

생각해보니 ABI 로 콜함으로서 전부 raw call로 함수를 불렀어야했다.

IVault(vaultAddr).addAmount{value: msg.value}(gameId);
라고 쓰면될걸

 

(bool success, ) = vaultAddr.call{value: msg.value}(
abi.encodeWithSignature("addAmount(uint256)", gameId)
);
require(success, "Failed to addAmount vault");

라고 써야해서

결과적으로는 남이 코드를 읽기 더어렵게 만들었을 뿐...


그럼 raw call 은 언제 쓰는가?

consensys 베스트 프랙티스에 따르면

It's recommended to stop using .transfer() and .send() and instead use .call().

송금시 raw call을 쓰라고한다.

One of the changes included in EIP 1884 is an increase to the gas cost of the 
SLOAD operation, causing a contract's fallback function to cost more than 2300 gas.

 .transfer() and .send() 은 2300 가스로 픽스가되어있어 이스탄불 업데이트 이후 (2019) 해당 펑션 이용시 가스비가 부족하기 때문이다.

 

아니 근데, 예제는 transfer 이나 send 하는 상황이아니잖아? raw call 의 이점이 뭐냐니까?


Raw call 의 이점: 에러 핸들링

address.call을 하면 성공/실패 여부를 bool로 받을수 있다.

실패시 내가 케이스 관리를 해야한다/할수있다.

// good
(bool success, ) = someAddress.call.value(55)("");
if(!success) {
    // handle failure code
}

컨트랙트.function() 도 당연히 성공, 실패 여부를 알수 있지만 실패시 throw an exception이어서 다음 코드가 실행되지 않는다.

 

 

내가 실패 케이스를 따로 관리(재시도를 한다거나)하는 시나리오가 아니라면

컨트랙트.function() 를 사용하는게 나을수 있다.

일단 심플하다

 


그리고 raw call을 하던 contract.call을 하던 외부 컨트랙트를 부를 때 더 신경써야하는 부분이 있다.

이 코드는 옥션 입찰에 상위입찰이 들어오면 그 전의 1등에게 출금을 해주려한다.

// bad
contract auction {
    address highestBidder;
    uint highestBid;

    function bid() payable {
        require(msg.value >= highestBid);

        if (highestBidder != address(0)) {
            (bool success, ) = highestBidder.call.value(highestBid)("");
            require(success); // if this call consistently fails, no one else can bid
        }

       highestBidder = msg.sender;
       highestBid = msg.value;
    }
}

근데 외부 컨트랙트에서 실패가 뜨면, require(success); 에 막혀서 bid()함수 자체가 작동하지 않게된다.

내 컨트랙트 탓이 아닌 외부 컨트랙트의 상태에 따라 좌지우지되는것.

 

외부 컨트랙트를 부르는 기능을 분리하자.

// good
contract auction {
    address highestBidder;
    uint highestBid;
    mapping(address => uint) refunds;

    function bid() payable external {
        require(msg.value >= highestBid);

        if (highestBidder != address(0)) {
            refunds[highestBidder] += highestBid; // record the refund that this user can claim
        }

        highestBidder = msg.sender;
        highestBid = msg.value;
    }

    function withdrawRefund() external {
        uint refund = refunds[msg.sender];
        refunds[msg.sender] = 0;
        (bool success, ) = msg.sender.call.value(refund)("");
        require(success);
    }
}

상위입찰자가 갱신되면 매핑에 환불할 지갑과 주소를 저장하고,

예전 입찰자들이 withdrawRefund()로 환불을 요청하면 그때 외부 함수를 부르는식이다.

이러면 외부 컨트랙트에 문제가 생겨도 입찰함수는 영향을 받지않고 출금함수만 영향을 받는다.

 

이러면 UI 단에 출금버튼이 하나 더생겨야한다. 기존에는 신청 안하고 가만히 있어도 알아서지갑에 환불되는방식이었음.

 

이미지화하면 이런식이다. (딱맞진않음 그냥 add claim 분리한다는의미)

 

(근데 withdrawRefund() 내에 refund금액이 0인지 확인안하고 그냥 송금해도되나?

하긴 다른 컨트랙트에서 예외처리해놨을수도)

 

다만 이렇게하면 가스비가 추가로든다

심지어 매핑에 저장하는것은 스토리지 저장이어서 비싸다. 저장에(SSTORE) 대략 20,000 gas, 업데이트에 5,000 gas

그니까 외부 컨트랙트를 부른다고 전부 매핑으로 저장하는게 아니고 돈과 관련된 필수 기능만 매핑해서 저장하는게 옳아보인다.

 

가스비

  • 20000 per 32 bytes with an overhead of 20000 for the length.
  • Plus 68 gas per byte (2176 gas per 32 bytes) for storing the calldata