//SPDX-License-Identifier: MIT
pragma solidity >=0.8.0 <0.9.0;
contract CheckContract {
function isContract(address _addr) private returns (bool isContract) {
uint32 size;
// 스마트 컨트랙트 상에서 어셈블리 코드를 이용할 수 있다.
assembly {
// extcodesize()는 옵코드 명령어로 해당 주소의 코드 스테이트 정보의 코드 사이즈를 가져올 수 있다.
size := extcodesize(_addr);
}
return (size > 0); // 0보다 크면 CA, 작으면 EOA
}
}
//SPDX-License-Identifier: MIT
pragma solidity >=0.8.0 <0.9.0;
import "hardhat/console.sol";
contract CheckContract {
// 이더를 전송하는 방법에는 세 가지 방법이 있다.
// 1. transfer: address(to).transfer
// 2. send: address(to).send
// 3. call: address(to).call
event TransferEvent();
function useTransfer(address payable to) public payable{
to.transfer(msg.value);
}
function useSend(address payable to) public payable{
// send()는 실패 여부를 리턴받을 수 있다.
bool result = to.send(msg.value);
if(result == true){
emit TransferEvent();
}else{
revert();
}
}
function useCall(address payable to) public payable{
// call()은 이더리움을 전송할 수 있을 뿐만 아니라 외부 함수를 호출할 수 도 있다.
// call()은 result와 더불어 실행한 결과값을 받을 수 있다.
// 이더 전송을 하지않을 경우{value:msg.value} 부분은 생략할 수 있다.
(bool result, bytes memory data) = to.call{ value: msg.value }("");
if(result == true){
emit TransferEvent();
}else{
revert();
}
}
}
//SPDX-License-Identifier: MIT
pragma solidity >=0.8.0 <0.9.0;
import "hardhat/console.sol";
// 호출을 당할 컨트랙트
contract Calculation {
event callNotExistFuncEvent();
// 다른 컨트랙트에서 호출을 당할 함수
function plusData(uint a, uint b) external pure returns (uint) {
return a + b;
}
fallback() external payable {
emit callNotExistFuncEvent();
}
}
// 호출을 할 컨트랙트
contract Caller {
// CA 체크 함수
function checkCaContract(address _addr) private view returns (bool isContract) {
uint32 size;
// 스마트 컨트랙트 상에서 어셈블리 코드를 이용할 수 있다.
assembly {
// extcodesize()는 옵코드 명령어로 해당 주소의 코드 스테이트 정보의 코드 사이즈를 가져올 수 있다.
size := extcodesize(_addr)
}
return (size > 0); // 0보다 크면 CA, 작으면 EOA
}
// 외부 컨트랙트의 함수를 호출할 함수
function callFunc(address payable _address, uint _a, uint _b) public returns(bytes memory) {
// 함수를 호출할때 들어갈 인풋 데이터의 메세지 값을 동일하게 생성해서 전달해줘야한다.
// 그 인풋 데이터 메세지 값은 abi.encodeWithSignature() 내부 함수를 사용하여 만들 수 있다.
// encodeWithSignature()의 첫 번째 인자는 띄어쓰기를 사용하면 안된다.
// 함수명과 함수의 파라메터 타입을 적는다. uint는 실제로는 uint256이기 때문에 uint256으로 명시해야한다.
// 두, 세 번째 파라메터는 실제로 전달할 파라메터이다.
bytes memory callFuncBytes = abi.encodeWithSignature("plusData(uint256,uint256",_a,_b);
if(checkCaContract(_address)) {
(bool result, bytes memory sum) = _address.call(callFuncBytes);
return sum;
}else{
revert();
}
}
// 외부 컨트랙트의 존재하지 않는 함수를 호출할 함수
function callNotExistFunc(address payable _address, uint _a, uint _b) public returns(bytes memory) {
// 존재하지 않는 함수를 인위적으로 호출해보자
bytes memory callFuncBytes = abi.encodeWithSignature("NotExistFunc(uint256,uint256",_a,_b);
if(checkCaContract(_address)) {
(bool result, bytes memory sum) = _address.call(callFuncBytes);
return sum;
}else{
revert();
}
}
}
각각의 컨트랙트를 배포하고 호출 당할 컨트랙트 주소를 복사하여 파라메터로 입력하고, 덧셈할 파라메터도 적용해서 트랜잭션을 발생시킨다. (트랜잭션 정보를 자세히 보기 위하여 메타마스크 bsc 테스트넷을 사용하였다.)
Input Data에 들어가있는 “0xa72d24730000000000000…”는 아래의 코드의 정보가 들어간 것이다.
callFunc(address payable _address, uint _a, uint _b)
callFuncBytes의 값은 인터널 트랜잭션을 호출할때 포함된다.
bytes memory callFuncBytes = abi.encodeWithSignature("NotExistFunc(uint256,uint256",_a,_b);
인터널 트랜잭션 탭에서 컨트랙트가 컨트랙트를 호출했음을 나타내는 정보를 확인할 수 있다.
호출을 당하는 Calculation 컨트랙트의 주소는 0xA7127..7842이고
호출을 하는 Caller 컨트랙트의 주소는 0xC06FE..238b이였다.
call() 함수가 메세지 형태로 호출하였음을 보여준다.
callNotExistFunc()의 호출 결과에서는 당연히 없는 함수 정보를 콜하였기 때문에 callNotExistFuncEvent 이벤트가 발생하며 이는 로그에서도 확인할 수 있다.
DelegateCall() 함수와 call() 함수의 차이
call()은 호출 당하는 컨트랙트의 인스턴스에서 데이터가 처리되는 반면
delegateCall()은 호출하는 외부 컨트랙의 함수를 마치 자기 자신의 멤버 함수를 호출하듯이 처리한다고 볼 수 있다.
데이터 처리가 어느 인스턴스에서 처리되느냐에 따라 이 둘을 구분해서 사용할 수 있겠다.
//SPDX-License-Identifier: MIT
pragma solidity >=0.8.0 <0.9.0;
import "hardhat/console.sol";
contract Calculation {
address public owner;
uint public value;
event callNotExistFuncEvent();
function plusData(uint a, uint b) external pure returns (uint) {
return a + b;
}
fallback() external payable {
owner = msg.sender;
value = msg.value;
}
}
contract DelegateCaller {
address public owner;
uint public value;
function checkCaContract(address _addr) private view returns (bool isContract) {
uint32 size;
// 스마트 컨트랙트 상에서 어셈블리 코드를 이용할 수 있다.
assembly {
// extcodesize()는 옵코드 명령어로 해당 주소의 코드 스테이트 정보의 코드 사이즈를 가져올 수 있다.
size := extcodesize(_addr)
}
return (size > 0); // 0보다 크면 CA, 작으면 EOA
}
function delegateCallFunc(address payable _address, uint _a, uint _b) public returns(bytes memory) {
bytes memory callFuncBytes = abi.encodeWithSignature("plusData(uint256,uint256",_a,_b);
if(checkCaContract(_address)) {
(bool result, bytes memory sum) = _address.delegatecall(callFuncBytes);
return sum;
}else{
revert();
}
}
function delegateCallNotExistFunc(address payable _address, uint _a, uint _b) public payable returns(bytes memory) {
// 존재하지 않는 함수를 인위적으로 호출해보자
bytes memory callFuncBytes = abi.encodeWithSignature("NotExistFunc(uint256,uint256",_a,_b);
if(checkCaContract(_address)) {
(bool result, bytes memory sum) = _address.delegatecall(callFuncBytes);
return sum;
}else{
revert();
}
}
}
delegateCallNotExistFunc()을 호출하면 없는 함수를 호출한 것이기 때문에 Calculation 컨트랙트의 fallback() 함수를 호출시키게 되고 fallback() 함수 내부에는 아래와 같이 owner, value 값을 저장하게되는데
fallback() external payable {
owner = msg.sender;
value = msg.value;
}
앞서 설명한것처럼 delegatecall()은 호출 당한 컨트랙트의 함수를 자신의 인스턴스에서 처리하게 되므로 owner 정보는 자신의 컨트랙트 스토리지 변수에 저장된것을 확인할 수 있다. (호출당한 컨트랙트의 스토리지 변수에는 값이 등록되지 않은것을 볼 수 있다.)(이더를 전송하면 해결할 수 없는 오류가 발생하여 value는 확인할 수 없었지만 현재 처리할 수 없는 코드상의 오류일뿐 밸류 또한 자신의 스토리지 변수에서 확인할 수 있다.)
이런 기술들은 이더리움 트랜잭션 서비스의 메타 트랜잭션에서 그대로 사용된다.
이더리움 트랜잭션 구조에서 사용자 from 주소를 알 수 없어서 사용자 암호화 값인 시그니처 rsv값에서 구해야 하는데 그 부분은 위 캡쳐 이미지에서 verify() 함수에 구현된 내용과 같다.
전달 받은 메세지 값(from, to, value, … , data)들을 해시한 값에다가 서명값과 함께 recover를 하게되면 사용자의 퍼블릭키(요청자의 지갑 주소)를 얻을 수 있다.