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() 세가지 방법이 있으며
기본적인 호출 형태는 다음과 같다
(bool success, bytes memory data) = contract_address.call(abi.encodeWithSignature("function(data_type)", [pamrms]));컨트랙트의 기능을 실행하면 bool과 bytes 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()은 호출 대상 컨트랙트를 실행하고 상태를 변경할 수 있다.
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.sender는 remotecall()를 실행시킨 컨트랙트의 주소 정보이다.delegatecall()는 호출 대상 컨트랙트를 실행은 가능하지만, 호출 대상 컨트랙트의 상태는 변경할 수 없다
Target 컨트랙트에서 msg.sender, msg.data는 Caller의 msg.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.sender는 remotedelegatecall()를 실행시킨 msg.sender, 즉 트랜젝션을 실행시킨 address라는 것을 확인할 수 있다.staticcall()은 상태 변경을 하지 않고 조회만 가능하다 view, pure 속성의 함수에서만 사용할 수 있다.
다른 컨트랙트의 인터페이스를 정의할 때 띄어쓰기를 할 경우 오류가 발생한다
contract_address.call(
abi.encodeWithSignature("set(uint256, address)", _amoumt, address),
); // 에러 발생
contract_address.call(
abi.encodeWithSignature("set(uint256,address)", _amoumt, address),
); // 정상 동작