[Solidity]Addresses & Accounts

끼리·2022년 3월 26일
0

솔리디티 공부

목록 보기
4/4

이더리움에서 계정은 두 가지 유형으로 구분된다. 외부 소유 계정 및 컨트랙트 계정. EVM은 기본적으로 동일하게 취급하기 때문에 이러한 계정 간의 차이는 개념의 차이이다.

EVM의 모든 계정에는 퍼블릭 주소와 잔액(balance)이 있다. 컨트랙트 계정에는 내부 storage 데이터뿐만 아니라 바이트 코드도 저장된다.

EOA에서 컨트랙트 어카운트로 호출할 때는 호출 한 계정 및, 호출을 한 계정의 수, 어떤 argument로 함수를 호출하려고 하는지 등을 파악하는 것이 중요하다.

Solidity 언어는 계약에서 정의한 기능에 대한 트랜잭션 데이터를 다룬다. 또한 msg.sender 및 msg.value와 같은 글로벌을 통해 트랜잭션 파라미터에 액세스할 수 있다.

이러한 유틸리티를 계정으로 작업하면서, 역할, 권한을 간단하게 정의하고, 계약에서의 토큰 잔액을 추적할 수 있다. Solidity에서 계정을 사용하는 방법에 대해 배워보자

Solidity Addresses

Solidity의 주소 데이터 유형에 대해 알아보자.

EVM 상의 주소는 160바이트 길이 또는 40자의 16진수 문자열임을 알 수 있다.

address a = 0xc783df8a850f42e7f7e57013759caa701eb6;

이것은 유효한 Solidity입니다! 필요하다면 컨트랙트에 고정 주소를 저장할 수 있다.

또한 현재 메시지의 발송인을 찾을 수 있다.

import "hardhat/console.sol";
contract Example {
    constructor() {
        console.log( msg.sender ); // 0xc783df8a850f42e7f7e57013759c285caa701eb6
    }
}

위 코드는 이 계약을 호출하는 계정의 주소를 확인한다.

여기서 msg는 무엇일까?

이더리움 메세지에 대해 알아보자면,

Ethereum Messages

외부 소유 계정이 있고 Ethernet Network와 통신하고 싶을 때 우리는 트랜잭션을 브로드캐스트한다. 이 트랜잭션에서는 EVM과 상호 작용하기 위한 바이트 코드인 데이터를 전송할 수 있다.

데이터를 전송하지 않으면 EVM과 상호 작용할 의도가 없다. 예시로, 1 개의 주소로부터 다른 주소로 심플한 Ether 전송의 경우다.

흔히 calldata라고 불리는 이 데이터는 메시지를 EVM에 전달하는 데 사용된다. 특정 계약 계정(Solidity 용어로 계약 또는 라이브러리 중 하나)을 대상으로 하며, 다른 계약 계정으로 호출할 수 있다. 컨트랙트 계정이 다른 컨트랙트 계정으로 호출될 때마다 메시지가 생성된다. 이 메시지에는 sender 주소, targeted 시그니처 및 전송된 wei 양이 포함된다.

Solidity에서는 글로벌 변수를 통해 다음 메시지에 액세스할 수 있다.

msg.data(bytes) - call data
msg.disc(address payable) - 메시지를 보내는 주소
msg.sig (bytes4) - 타깃 함수시그니처
msg.value (uint) - 전송된 wei의 양

msg.sig가 4바이트인 이유는 이 값은 실제로 함수 시그니처의 keccak256 해시의 첫 번째 4바이트이기 때문이다. function 시그니처의 길이를 신경쓰지 않고 스마트 컨트랙트상의 함수를특정(및 타겟팅)할 수 있다. 그렇지 않으면 reallyLongNameForAFunction 을 저장할 수 있으며, 해당 함수를 호출하려면 콜데이터 가 이 모든 정보를 저장해야 한다.

해당 내용 예제 코드

Store the Owner

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
import "hardhat/console.sol";
contract Contract {
    address public owner ;
    constructor() {
      owner= msg.sender ;
    }
}

Address Payable

Solidity 주소에는 그냥 address와 address payable의 두 가지 유형이 있다.

두 주소의 차이는, address payable은 transfer과 send을 가지고 있다는 것이다.

이 두 주소의 구별 목적은 개발자가 address로 지불하려고 할때이다. 이로 인해 실수가 발생할 가능성이 낮아진다.

아래코드는 payable 코드로 변환하는 것이다.


contract Example {
    address payable a;
    address b;
    address payable c;

    constructor(
        address payable _a, 
        address payable _b, 
        address _c
    ) {
        a = _a; // store payable in payable
        b = _b; // implicit conversion to nonpayable
        c = payable(_c); // explicit conversion to payable
    } 
}

첫 번째 할당은 address payable 변수에 address payable한 주소를 저장합니다. 변환이 불필요!

다음 두 가지 할당은 데이터 유형 변환을 보여 준다.

Payable Owner

예제코드

pragma solidity ^0.8.4;
import "hardhat/console.sol";
contract Contract {
    address payable public owner ;
    constructor() {
      owner= payable(msg.sender) ;
    }
}

Receive Function

Solidity의 최신 버전에서는 기본적으로 계약이 ether를 받을 수 없다.

ether를 받을라면 컨트랙트에 payable한 함수를 지정해야 한다. 이는 view 및 pure 와 유사한 함수의 가변성에 영향을 미치는 또 다른 키워드다.

실제로 ABI에서 함수의 stateMutability는 view, pure, payable 및 nonpayable의 4가지 값 중 하나다. 마지막값이 지정하지 않았을 시 기본값이다.

payable한 함수가 어떻게 작동하는지 살펴보면 다음과 같다.

import "hardhat/console.sol";
contract Contract {
    function pay() public payable {
        console.log( msg.value ); // 100000
    }
}

여기서 msg.value는 Wei 단위로 측정된 ether의 양이다. 이 함수에 payable 키워드를 추가하는 것만으로 ether를 받을 수 있다. 컨트랙트 잔액에 자동으로 저장되므로 다른 작업을 할 필요가 없다!

만약 누군가가 nonpayable한 함수로 지불을 보내려고 했다면? 트랜잭션은 실패하고, sender에게 Ether를 반송합니다.

위의 경우 payable 함수로서 pay방법을 사용했다. 즉, 컨트랙트에 ether를 전송하기 위해서는 이 함수를 호출해야 한다. method를 지정하지 않고 직접 보내려면 어떻게 해야 할까?

import "hardhat/console.sol";
contract Contract {
    receive() external payable {
        console.log(msg.value); // 100000
    }
}

recieve()에서는 function 키워드가 사용되지 않는다. 이는 constructor과 같은 특수 함수이기 때문이다. calldata 없이 컨트랙트를 보낼 때 실행되는 기능이다.

receive 함수는 external,payable여야 하며, 인수를 받을 수 없으며 아무것도 반환할 수 없다.

계약에 함수 시그니처를 지정하지 않고 ether를 수신하는 또 다른 방법은, payable fallvack function을 사용하는 것이다.

External Visibility

external은 무엇이며, receive 함수에 필요한 이유는 무엇일까?

External은 EOAs에서만 함수를 호출할 수 있음을 의미한다. 어떤 계약이나 라이브러리에서도 내부적으로 호출할 수 없다.

특히 receive 기능은 내부 메시지에 응답하기 위한 것이 아니기 때문에 external visibility을 필요로 한다. 컨트랙트 계좌로 전송될 때 개발자가 로직을 작성할 수 있는 function body를 제공하는 것이 유일한 목적이다.

마찬가지로 fallback 함수에도 external visibiliy이 필요합니다. 대체 기능은 무엇입니까? 계속 읽어봐, 친구!

Fallback Function

가짜 바이트 코드 데이터로 스마트 계약 함수로 호출하면 폴백 함수가 실행된다. 이는 잘못된 함수 이름을 입력하거나 잘못된 인수 유형을 지정하는 단순한 실수일 수 있다. 바이트 코드 0xdeadbeef일 수도 있다.

컨트랙트에 전송된 데이터에 respond 하는 방법을 모를 경우, 폴백 기능을 호출한다.

폴백 기능도 특수 기능으로 다음과 같습니다.

contract Contract {
    fallback() external {
        // do somethin'
    }
}

reciece 함수와 마찬가지로 폴백 함수는 external여야 하며 인수를 받아들이거나 값을 반환할 수 없다. receive 기능과 달리 폴백은 payable할 필요가 없다.

payable 폴백 기능을 사용하면 receive 기능을 대체할 수 있지만, 그런 경우 는 거의 볼 수 없다. 두 기능은 서로 다른 용도로 사용되기 때문이다.

receive 함수를 생성하면 데이터가 없는 트랜잭션에서 ether를 수락하는 것이 분명하다.

그러나 폴백 함수를 생성하면 일반적으로 함수 시그니처 오류를 처리하기 위한 것이다.

Receive Ether

예제코드

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
import "hardhat/console.sol";
contract Contract {
   address payable public owner ;
   receive() external payable {
     owner= payable(msg.sender) ;
    }
}

Transferring Funds

우리는 어떤 함수라도 payable하게 할 수 있다. 이를 통해 스마트 컨트랙트에 들어가는 ether의 목적을 구별할 수 있다.

아마도 컨트랙트에 주소가 두 개 있고, 우리는 둘 중 하나를 지불하고 싶었을 것이다.

contract Contract {
    address payable public a;
    address payable public b;
    constructor(address _a, address _b) {
        a = _a;
        b = _b;
    }
    function payA() public payable {
        a.transfer(msg.value);
    }
    function payB() public payable {
        b.transfer(msg.value);
    }
}

payA와 payB의 두 가지 지불방법이 있으며, 각각의 주소로 ether를 전송합니다. transfer 메서드는 모든 address payable 에서 사용할 수 있다. Wei의 금액만큼을 컨트랙트 주소로부터 송금한다.

address에서 사용할 수 있는 다른 기능은 send이다. 이 함수는 장애 발생 시 오류가 발생하지 않는다는 점을 제외하고는 transfer과 동일하다. 에러 처리에 대해서는 아직 논의하지 않았으나, 주의할 필요가 있다.

이더를 전송하려는 주소가 컨트랙트일 경우, transfer과 send 모두 실행 가능한 옵션이 아니다. 그 이유는 무엇일까? Transfer and Send with Fallbacks에 대해 알아보자

Transfer and Send with Fallbacks

import "hardhat/console.sol";
contract Contract {
    receive() external payable {
        console.log("I received ether!"); // I received ether!
    }
}

여기서 문제는 sendtransfer 이 모두 얼마나 많은 가스을 사용할것이지에 대해 까다롭다는 것이다. 두개 다 2300개의 가스만 사용한다. 이 양은 재귀 콜을 만들기에 충분한 가스를 제공하지 않기 때문에 re-entrancy(재진입)방지하기 위해 의도적으로 조정할 수 없다.

re-entrancy(재진입)은 전송 순서와 상태 변수 갱신 순서에 따라 발생할 수 있는 재귀적인 계약 버그이다. DAO 해킹을 일으킨 것으로 악명 높은 버그이기도 하다. 이로 인해 DAO 하드 포크가 도입되어 이더리움이 두 개의 활성 네트워크로 분할되었다. 이더리움과 이더리움 클래식

The Potential Problem

2300 가스는 조정이 불가능하지만 EVM opcode 가스 비용은 종종 변경되기 때문에 한 번 작동했던 transfer 또는 send 이 향후 이더리움 업데이트와 함께 중단될 수 있다.

계약의 폴백 기능을 수행하기 위해 2100개의 가스가 필요한지 생각해 보자. 폴백 기능의 운영 코드 중 하나에 대한 비용이 300 가스 증가하면 어떻게 될까? 이제 이를 실행하려면 2400개의 가스가 필요합니다. 현재 이 폴백 기능은 2300만 지급된다면 가스가 고갈될 것이다.

현재 사용 가능한 모든 가스를 전송하는 Solidity에서 이더를 전송하는 다른 방법은 다음과 같다.

import "hardhat/console.sol";
contract Contract {
    address public otherContract;
    constructor(address _otherContract) {
        otherContract = _otherContract;
    }
    function payA() public payable {
        // forwards all remaining gas along 
        (bool success, ) = otherContract.call{ value: msg.value }("");
        // should always handle the failure case here 
        // compiler will warn if you do not define this success bool
        console.log(success); // true for success, false for failure
    }
}

Transfer Tips

예제코드

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
import "hardhat/console.sol";
contract Contract {
   address payable public owner ;
   receive() external payable {
     owner= payable(msg.sender) ;
    }
     function tip() public payable {
        owner.transfer(msg.value);
    }
}

Contract Account

계약내에서는, this 키워드를 주소로 변환할 수 있다.

import "hardhat/console.sol";
contract Contract {
	constructor() {
		console.log( address(this) ); // 0x7c2c195cd6d34b8f845992d380aadb2730bb9c6f
		console.log( address(this).balance ); // 0
	}
}

this를 사용함으로써 컨트랙트 주소나 잔액을 쉽게 알 수 있다

This Keyword ?

Solidity에서는 this 키워드를 사용하면 컨트랙트 자체에 액세스할 수 있다.
.(점)연산자를 사용하여 함수를 호출할 수 있다.

import "hardhat/console.sol";
contract Example() {
    function a() public view {
        console.log( this.b() ); // 3
    }
    function b() public pure returns(uint) {
        return 3;
    }
}

이것은 외부함수이기도 하다.

라이브러리 함수 호출 예제를 보면,

import "./UIntFunctions.sol";
import "hardhat/console.sol";
contract Example {
    constructor() {
        console.log( UIntFunctions.isEven(2) ); // true
        console.log( UIntFunctions.isEven(3) ); // false
    }
}

this 타입을 명시적으로 address로 변환할 수도 있다.

import "./UIntFunctions.sol";
import "hardhat/console.sol";
contract Example {
    constructor() {
        console.log( address(this) ); // 0x8858eeb3dfffa017d4bce9801d340d36cf895ccf
        console.log( address(this).balance ); // 100000000000000000
        console.log( address(UIntFunctions) ); // 0x7c2c195cd6d34b8f845992d380aadb2730bb9c6f
    }
}

this를 주소로 변환하면, 다른 주소와 같이 취급할 수 있다. 이것은 컨트랙트 계정 자체의 주소를 나타낸다.

Charity Donation

예제코드

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

contract Contract {
	address payable public owner;
	address payable public charity;

	constructor(address payable _charity) {
		owner = payable(msg.sender);
		charity = _charity;
	}

	receive() external payable { }

	function donate() public {
		charity.transfer(address(this).balance);
	}

	function tip() public payable {
		owner.transfer(msg.value);
	}
}

Self Destruct

컨트랙트는 EVM의 SELFDESTRUCT opcode 코드를 사용하여 없앨 수 있다.

이 opcode는 사용하지 않는 컨트랙트에서 블록체인을 정리하도록 실제로 ether를 환불한다.

실제 코드를 보면

contract Contract {
    uint _countdown = 10;

    constructor() payable { }

    function tick() public {
        _countdown--;
        if(_countdown == 0) {
            selfdestruct(msg.sender);
        }
    }
}

tick 함수를 10번 호출하면 계약이 자동으로 파기된다.

여기서 msg.send를 사용한이유는 다음과같다.

selfdestruct 함수에 제공된 주소는 컨트랙트에 남아 있는 모든 ether을 가져온다! payable한 생성자로 전송되는 Ether는 tick 함수의 최종 발신자에게 환불된다.

하지만 스마트 컨트랙트를 파기하기 전에 결과를 고려해 보는 것이 좋다.

Self-Destruct Repercussions

컨트랙트 계정에서 selfdestruct를 호출하면 바이트 코드가 지워진다. 컨트랙트는 더 이상 ether 전송에 응답할 수 없다.

만약 selfdestruct를 사용한다면, 미래에 아무도해당 계약에 ether를 보내지 않도록 해야 한다. 만약 그런일이 생긴다면, 해당 ether을 되찾을 방법이 없을지도 모른다. 앞으로 이 주소로 보내지는 ether는 영원히 꺼내지못한다

바이트 코드가 클리어되면 EIP-1014에서 도입된 CREATE2 opcode를 사용하여 동일한 주소에 동일한 코드를 배치할 수 있다. 컨트랙트의 주소는 발송인의 주소와 계정 nonce에 의해 결정된다. nonce 대신 CREATE2 opcode가 salt와 컨트랙트 생성 코드를 사용한다.
주의: 최초 배포에 CREATE2를 사용한 경우에만 같은 주소로 재배포할 수 있다.

계약을 스스로 파기하는 대신 아무도 함수를 호출할 수 없도록 상태 변수를 설정하는 것을 고려할 수 있다. 그런 다음 나중에 함수를 호출하거나 ether를 전송하려고 하면 트랜잭션을 되돌린다.

Self Destruct

예제코드

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

contract Contract {
	address payable public owner;
	address payable public charity;

	constructor(address payable _charity) {
		owner = payable(msg.sender);
		charity = _charity;
	}

	receive() external payable { }

	function donate() public {
		selfdestruct(charity);
	}

	function tip() public payable {
		owner.transfer(msg.value);
	}
}

0개의 댓글