All Articles

contract call

테스트 컨트랙트

contract Target {

    address _last_sender;

    constructor () public {
        _last_sender = msg.sender;
    }

    function set() public returns (address) {
        _last_sender = msg.sender;
        return msg.sender;
    }

    function get() public view returns (address) {
        return _last_sender;
    }
}

set()를 호출하면 msg.sender의 주소를 _last_sender에 저장하고 msg.sender를 반환하는 간단한 contract를 통해 호출 구조를 확인할 수 있다

코드 테스트를 통해 동작 확인

describe("target contract 실행", () => {
  it("set", async () => {
    let last_sender = await this.target.get();
    assert(last_sender.should.be.equal(owner1));

    await this.target.set({ from: user1 });

    last_sender = await this.target.get();
    assert(last_sender.should.be.equal(user1));
  });
});

.call() / .delegatecall() / .staticcall()

컨트랙트를 호출하는 방법으로 .call() / .delegatecall() / .staticcall() 세가지 방법이 있으며

기본적인 호출 형태는 다음과 같다

(bool success, bytes memory data) = contract_address.call(abi.encodeWithSignature("function(data_type)", [pamrms]));

컨트랙트의 기능을 실행하면 boolbytes type의 두개의 데이터가 반환되며 각각 실행 성공 여부와 반환되는 데이터를 의미한다

bytes 형태로 반환된 데이터는 abi.decode()를 통해 디코딩 후 사용 가능하다

function get() public view returns (address, address[], uint256) {}

위오 같은 함수를 호출한 결과를 디코딩 하려면 다음과 같이 변환해 사용할 수 있다

(address a, address[] b, uint256 c) = abi.decode(data, (address, address[], uint256));

Target 컨트랙트를 호출하는 Caller 컨트랙트를 통해 .call() / .delegatecall() / .staticcall()의 차이점을 확인할 수 있다

contract Caller {
    address _sender;
    function remotecall(address _contract) public {
        (bool success, bytes memory data) = _contract.call(abi.encodeWithSignature("set()"));
        require(success, "contract call failed");
        (address a) = abi.decode(data, (address));
        _sender = a;
    }

    function remotedelegatecall(address _contract) public {
        (bool success, bytes memory data) = _contract.delegatecall(abi.encodeWithSignature("set()"));
        require(success, "contract delegatecall failed");
        (address a) = abi.decode(data, (address));
        _sender = a;
    }

    function get() public view returns (address) {
        return _sender;
    }

    function remotestaticcall(address _contract) public view returns (address) {
        (bool success, bytes memory data) = _contract.staticcall(abi.encodeWithSignature("get()"));
        require(success, "contract staticcall failed");
        (address a) = abi.decode(data, (address));
        return a;
    }
}
.call()

.call()은 호출 대상 컨트랙트를 실행하고 상태를 변경할 수 있다.

Target 컨트랙트에서 조회되는 msg.sender.call()을 실행시킨 컨트랙트의 주소 정보를 가지게 된다

await this.caller.remotecall(this.target.address, { from: user1 });
const address1 = await this.target.get();
assert(address1.should.be.equal(this.caller.address));

const address2 = await this.caller.get();
assert(address2.should.be.equal(this.caller.address));
  • .call()를 호출하면 Target_last_sender를 변경시킬 수 있다
  • .call()를 통해 호출한 set()msg.senderremotecall()를 실행시킨 컨트랙트의 주소 정보이다
.delegatecall()

.delegatecall()는 호출 대상 컨트랙트를 실행은 가능하지만, 호출 대상 컨트랙트의 상태는 변경할 수 없다

Target 컨트랙트에서 msg.sender, msg.dataCallermsg.sender, msg.data의 정보와 같다

await this.caller.remotedelegatecall(this.target.address, { from: user1 });
const address1 = await this.target.get();
assert(address1.should.be.equal(owner1));

const address2 = await this.caller.get();
assert(address2.should.be.equal(user1));
  • .delegatecall()를 호출해도 Target_last_sender는 변경되지 않는다
  • .delegatecall()를 통해 호출한 set()msg.senderremotedelegatecall()를 실행시킨 msg.sender, 즉 트랜젝션을 실행시킨 address라는 것을 확인할 수 있다
.staticcall()

.staticcall()은 상태 변경을 하지 않고 조회만 가능하다 view, pure 속성의 함수에서만 사용할 수 있다.

주의사항

다른 컨트랙트의 인터페이스를 정의할 때 띄어쓰기를 할 경우 오류가 발생한다

contract_address.call(
  abi.encodeWithSignature("set(uint256, address)", _amoumt, address),
); // 에러 발생

contract_address.call(
  abi.encodeWithSignature("set(uint256,address)", _amoumt, address),
); // 정상 동작

Published Apr 10, 2020

Right Thoughts, Right Words, Right Action