Smart Contract/Languages

Solidity

dev_swan 2023. 11. 9.

✏️ Solidity 문법

solidity - solidity

❗️변수

1️⃣ Local 변수

  • 함수 내부에서 선언합니다.
  • 블록체인에 저장되지 않습니다.

2️⃣ State 변수

  • 함수 외부에서 선언합니다.
  • 블록체인에 저장됩니다.

3️⃣ Global 변수

  • 블록체인에 대한 정보를 제공합니다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract Variables {
    // 블록체인에 저장되는 상태 변수입니다.
    string public text = "Hello";
    uint public num = 123;

    function doSomething() public {
        // 블록체인에 저장되지 않는 로컬 변수입니다.
        uint i = 456;

        // 블록체인에 대한 정보를 제공해주는 글로벌 변수입니다.
        uint timestamp = block.timestamp;
        address sender = msg.sender;
    }
}

❗️ 접근 제어자

1️⃣ Internal

Contract 내부 혹은 상속된 Contract에서 호출 가능합니다.

 

2️⃣ External

Contract 외부에서만 호출 가능합니다.

 

3️⃣ Public

해당 함수를 Contract 내부/외부 모두에서 호출 가능합니다.

 

4️⃣ Private

해당 함수를 Contract 내부에서만 호출 가능합니다.


❗️ 함수 타입 제어자

1️⃣ View

  • 함수밖에 데이터를 읽을 순 있으나 데이터를 변경할 수 없습니다.

2️⃣ Pure

  • 함수밖에 데이터를 읽지 못합니다.
  • 가스비가 발생하지 않습니다.
  • 인자값만 사용해서 반환값을 정합니다.

3️⃣ Payable

  • 함수가 ETH를 받을 수 있게 합니다.
  • 가스비가 발생합니다.

❗️ 값 타입

1️⃣ 논리 자료형 ( Boolean )

  • True 또는 False입니다.

2️⃣ 정수형 ( Int, Uint )

  • int8 ~ int256 : 8 ~ 256 Bit의 양수 음수 저장 가능합니다.
  • uint8 ~ uint256 : 8 ~ 256 Bit의 양수만 저장 가능합니다.

3️⃣ 주소형 ( Address )

  • address : 20Byte의 이더리움 지갑 주소입니다.
  • address(0) : 주소 0(0x00000000... 00)을 의미합니다. 주로 토큰을 소각할 때 사용합니다.
  • address(this) : 해당 Contract Address를 의미합니다.

4️⃣ 문자열 ( String )

  • string : String 타입은 가스비용이 더 들어가기 때문에 32Byte 이상일 때만 string 타입을 사용합니다.

❗️ 열거형 ( Enum )

서로 연관된 상수들을 묶어 값을 지정하는 자료형입니다.

enum medal {gold,silver,bronze } // gold = 0, silver = 1, bronze = 2
enum Ranking {gold = 1, silver, bronze } // gold = 1, silver = 2, bronze = 3

❗️ 참조 타입

1️⃣ Storage

  • 변수를 블록체인상에 영구히 저장합니다. / Ex : 하드디스크

2️⃣ Memory

  • 임시 저장 변수, 함수가 종료되면 휘발됩니다. / Ex : Ram

❗️ Struct ( 구조체 )

관련된 변수들을 하나의 그룹으로 묶어서 사용할 수 있는 사용자 정의 데이터 타입입니다.

struct Person {
    string name;
    uint8 age;
}

❗️ Array ( 배열 )

1️⃣ 동적배열

  • 배열의 길이가 무한대로 늘어납니다.
uint[] public arr;

 

2️⃣ 정적배열

  • 미리 배열의 최대 길이를 지정합니다.
uint[10] public arr

❗️ Mapping ( 매핑 )

  • Key & Value 형식으로 데이터를 저장합니다.
  • Key Type은 동적배열, 열거형, 구조체, 매핑을 제외한 모든 타입 가능합니다.
  • Value Type은 모든 타입 가능합니다.
mapping(address => uint256) balance;

❗️ Modifier ( 함수 변경자 )

Modifier를 사용하면 사전에 어떤 조건에 부합되는지 확인 가능합니다. 예를 들어 함수를 실행시키기 전, 스마트컨트랙트를 배포한 사람의 계정과 동일한 계정인지 확인 가능합니다.

modifier check {
    count ++;
    _;
    count --;
}

function setCount() check public {
   if(mutex == 1) exeCnt = exeCnt + 1;
}

❗️ Virtual , Override

  • Virtual Keyword를 사용한 함수는 덮어쓸 수 있습니다.
function balanceOf(address owner) public view virtual returns (uint256){
    require(owner != msg.sender);
    return 100;
}
  • Override Keyword를 사용한 함수는 덮어쓴 함수입니다.
function balanceOf(address owner) public view override returns (uint256){
    require(owner != msg.sender);
    uint256 a = 100
    return a;
}

❗️ Event , Emit , Indexed

  • Event를 사용하여 value를 확인할 수 있습니다.
  • Emit을 사용하여 Event를 블록에 담을 수 있습니다.
  • Indexed를 사용하면 블록에서 출력된 Event를 필터링하여 특정 Event만 확인할 수 있습니다.
contract eventTest {
    event testEvent(address wallet, uint256 money);
    event testEvent2(address indexed wallet, uint256 money);

    function sendMoney() public {
        emit testEvent(address(0),10000)
        emit testEvent2(address address(0), uint256 money);
    }
}

❗️ Event log 확인

서버에서 Transaction 내용을 가지고 Event log를 decode 하여 확인할 수 있습니다.

/** Transaction Sample */

/**
{
  blockHash: '0xb615dcd7cdd5155299cbd381f51315fad97115b6675c95006efa81df6bf0cc75',
  blockNumber: 16073981,
  contractAddress: null,
  cumulativeGasUsed: 12120252,
  effectiveGasPrice: 11115226078,
  from: '0xd6150f8b9e586fbc36f498ff9a1d50525e406fa8',
  gasUsed: 35293,
  logs: [
    {
      address: '0x57ad75C0D3C9b97Ad4Bc8225Fab4c9EFedbC81Ef',
      blockHash: '0xb615dcd7cdd5155299cbd381f51315fad97115b6675c95006efa81df6bf0cc75',
      blockNumber: 16073981,
      data: '0x0000000000000000000000000000000000000000000000004563918244f40000',
      logIndex: 257,
      removed: false,
      topics: [Array],
      transactionHash: '0xec98e7506fa59be6ea3c546e3ce2979b4d847e0784384af2e68fd7895858e127',
      transactionIndex: 128,
      id: 'log_bb167d08'
    }
  ],
  logsBloom: '0x00000000000000000200000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000020000000000000008000000000000000000008000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010008000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000010000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000',
  status: true,
  to: '0x57ad75c0d3c9b97ad4bc8225fab4c9efedbc81ef',
  transactionHash: '0xec98e7506fa59be6ea3c546e3ce2979b4d847e0784384af2e68fd7895858e127',
  transactionIndex: 128,
  type: '0x0'
}
*/

const eventLog = web3.eth.abi.decodeLog(
    [
        {
            type: 'address',
            name: 'from',
            indexed: true,
        },
        {
            type: 'address',
            name: 'to',
            indexed: true,
        },
        {
            type: 'uint256',
            name: 'value',
        },
    ],
    result.logs[0].data,
    [result.logs[0].topics[1], result.logs[0].topics[2]],
);

console.log(eventLog)

/**
Result {
  '0': '0xd6150F8b9E586FBc36F498FF9A1D50525E406fA8',
  '1': '0xe89a6122C127055FbDb3Df2BaEB3A4FfDaBb4711',
  '2': '5000000000000000000',
  __length__: 3,
  from: '0xd6150F8b9E586FBc36F498FF9A1D50525E406fA8',
  to: '0xe89a6122C127055FbDb3Df2BaEB3A4FfDaBb4711',
  value: '5000000000000000000'
}
*/

❗️ Abstract Function ( 추상 함수 )

  • Abstract Function는 함수의 이름과 입력 매개 변수와 출력 매개 변수만 선언해 두고 내용은 없는 함수입니다.
  • 내용이 없기 때문에 중괄호 끝에 ;만 붙이면 됩니다.
function name(x1,...,xN) Option returns(y1,...,yN)

❗️ Abstract Contract ( 추상 컨트랙트 )

  • Abstract Contract는 컨트랙트를 만드는 기본틀로 활용됩니다.
  • 단순히 컨트랙트 내부에서 하나 이상의 함수가 추상 함수이면 자동으로 추상 컨트랙트가 됩니다.
abstract contract Context {
    function _msgSender() internal view virtual returns (address) {
        return msg.sender;
    }

    function _msgData() internal view virtual returns (bytes calldata) {
        return msg.data;
    }
}

❗️ Using A For B

라이브러리 A의 모든 유형 B로 첨부합니다.

import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

contract LockupContract {
  using SafeERC20 for IERC20;
}

❗️ Super

유효성 검사를 포함하지 않기 때문에 일반 산술 연산에 비해 더 작은 바이트코드를 생성하여 가스비를 줄이는 역할을 합니다.

 

  • Use Unchecked
uint256 senderBalance = _balances[sender];
require(senderBalance >= amount, "ERC20 : transfer amount exceeds balance");

unchecked{
    // no validation here as it's a already validated in the `require()` condition
    _balances[sender] = senderBalance - amount;
}

 

  • Unused Unchecked
uint256 senderBalance = _balances[sender];
require(senderBalance >= amount, "ERC20 : transfer amount exceeds balance");

// redundant validation here
_balances[sender] = senderBalance - amount;

❗️ Assembly

어셈블리는 EVM 바이트코드를 더욱 직접적으로 제어하게 해주는 저수준 프로그래밍 요소로, 최적화 및 더 복잡한 연산을 수행하기 위해 사용됩니다.

 

  • inline Assembly안에서 변수를 할당할 때는 =가 아닌 :=로 할당해야 합니다.
assembly {
    let A := 5
}

 

  • assembly는 블록 간 namespace를 공유하지 않습니다.
assembly {
    let x := 5
}

assembly {
    let y := x  /** Error */
}

 

  • assembly를 사용하여 가스 비용을 절감할 수 있습니다.
function addAssembly(uint x, uint y) public pure returns (uint) {
     assembly {

       	 // 변수 result를 선언하여 :=를 통해 x+y 값을 변수에 할당
         let result := add(x, y)

       	 // mstore opcode를 사용하여 메모리 주소 0x0에 result 값을 저장합니다.
         mstore(0x0, result)

         // 0x0 메모리 주소에 32Byte를 반환합니다.
         return(0x0, 32)
     }
 }

 function addSolidity(uint x, uint y) public pure returns (uint) {
     return x + y;
 }

 

  • assembly 내부에서 문자열을 선언할 때 32Byte보다 클 경우 에러가 발생합니다.
assembly {
	let Text := '32Byte보다 큰 문자열' /** Error */
}

 

  • assembly 내부에서 변수는 블록 범위 지정의 표준 규칙을 따릅니다. 블록 범위는 {...}로 구분합니다.
assembly {
	let x := 5

  	{
    	let y := x
    }

  	{
    	let z := y /** Error */
    }
}

 

  • assembly에서 for문은 아래와 같이 작성할 수 있습니다.
assembly {
  		/**
        * { let i := 0 } = 변수 i 선언 및 0 할당
        * lt(i, n) = 조건으로 i < n을 의미하며 양수만 비교 가능합니다.
        * { i := add(i, 1) } = 반복 동작으로 i++을 의미합니다.
        */

      for { let i := 0 } lt(i, n) { i := add(i, 1) } {
           value := mul(2 , value)
       }

       mstore(0x0, value)

       return(0x0, 32)
}

 

  • assembly에서 if문은 아래와 같이 작성할 수 있습니다. else는 지원하지 않습니다.
assembly {
  	/** slt(x, 0) = x < 0을 의미하며 양수 음수 모두 비교 가능합니다. */
  	/** sub(0, x) = 0 - x를 의미합니다. */

    if slt(x, 0) {
      x := sub(0, x)
    }
}

 

  • assembly에서 switch문은 아래와 같이 작성할 수 있습니다. case문에서 모든 유형이 포함된 경우 default case는 허용되지 않습니다.
assembly {
    let a := 5

    switch lt(a,10)
    case true {
    	/** DO SOMETHING */
    }
    case false {
        /** DO SOMETHING */
    }
    defalut {
        /** This is not allowed */
    }
}

❗️ abi.encode, abi.encodePacked

1️⃣ ABI encode

solidity에서 제공하는 EVM에 대한 표준 인코딩 규칙으로 인코딩하여 EVM이 읽을 수 있도록 합니다.

 

2️⃣ ABI encodePacked

데이터를 인코딩하는데 필요한 최소한의 메모리만( 압축 인코딩 ) 사용합니다. 동적유형 (배열, 문자열 등)은 길이 없이 있는 그대로 인코딩 됩니다. 32 Bytes보다 짧은 유형은 0으로 채워지지 않습니다.

 

abi.encode("Test")
// bytes:0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000045465737400000000000000000000000000000000000000000000000000000000

abi.encodePacked("Test")
// bytes: 0x54657374

❗️ abi.encodeWithSelector, abi.encodeWithSignature, abi.encodeCall

 

1️⃣ ABI encodeWithSelector

call 메서드로 다른 컨트랙트의 기능을 호출할 때 사용하며, 2번째 매개변수 부터 인코드하여 함수 선택자(1번째 매개변수) 앞에 붙입니다.

interface IERC20 {
    function transfer(address, uint) external;
}

function encodeWithSelector(address to, uint amount) external pure returns (bytes memory) {
    return abi.encodeWithSelector(IERC20.transfer.selector, to, amount);
}

 

 

2️⃣ ABI encodeWithSignature

call 메소드로 다른 컨트랙트의 기능을 호출할 때 사용하며, 인자값으로 함수 서명과 함수 서명의 매개변수들이 차례로 들어갑니다. abi.encodeWithSelector(bytes4(keccak256(bytes(signature))), ...)와 동일합니다.

interface IERC20 {
    function transfer(address, uint) external;
}

function encodeWithSignature(address to, uint amount) external pure returns (bytes memory) {
    return abi.encodeWithSignature("transfer(addres,uint256)", to, amount);
}

 

3️⃣ ABI encodeCall

abi.encodeWithSelector와 유사하지만 제공된 값이 호출된 함수의 타입과 일치하는지 확인합니다.

interface IERC20 {
    function transfer(address, uint) external;
}

function encodeCall(address to, uint amount) external pure returns (bytes memory) {
    return abi.encodeCall(IERC20.transfer, (to, amount));
}

❗️ function signature

함수 서명은 함수와 함수가 사용하는 매개변수 타입의 조합입니다.

function transfer(address to, uint256 amount) public {
	// CODE
}

// function signature
transfer(address, uint256)

❗️ function selector

  • 함수 선택자는 호출할 함수를 지정하는 함수 호출 데이터의 처음 4Byte입니다.
  • EVM에서 스마트 컨트랙트를 실행한다고 하면 어떤 기능을 실행하는지 알려주는 역할입니다.
  • 함수 선택자는 동일한 함수 서명의 해시입니다.
bytes4(keccak256(bytes(function_signature)))

❗️ Call, delegate Call, static Call

1️⃣ call

외부 컨트랙트를 호출하는 기본적인 메서드로 A컨트랙트에서 B컨트랙트에 접근해 storage에 저장된 데이터를 변경할 수 있습니다. 이때 msg.sender는 A 컨트랙트의 주소가 됩니다.

 

2️⃣ delegate call

call 메서드와 비슷한데, 차이점은 A 지갑에서 A 컨트랙트를 이용해 B 컨트랙트를 호출할 때 msg.sender는 A 컨트랙트 주소가 아닌 A 지갑주소가 됩니다. 또한 데이터를 변경한다고 했을 때, B 컨트랙트의 데이터가 바뀌는 것이 아닌 A 컨트랙트의 데이터만 변경됩니다.

 

3️⃣ static call

함수 내에서 상태값을 변경하지 않는 pure 함수와 view 함수는 static call로 호출합니다.

댓글