ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [solidity] UniswapV3Factory 컨트랙트로 interface, abstract contract 살펴보기
    solidity 2023. 2. 14. 22:26
    반응형
    contract UniswapV3Factory is IUniswapV3Factory, UniswapV3PoolDeployer, NoDelegateCall {
        address public override owner;
        mapping(uint24 => int24) public override feeAmountTickSpacing;
        mapping(address => mapping(address => mapping(uint24 => address))) public override getPool;
    
        constructor() {...생략}
    
        function createPool(
            address tokenA,
            address tokenB,
            uint24 fee
        ) external override noDelegateCall returns (address pool) {
            require(tokenA != tokenB);
            ...생략
        }
    
        function setOwner(address _owner) external override {
            ...생략
        }
    
        function enableFeeAmount(uint24 fee, int24 tickSpacing) public override {
            require(msg.sender == owner);
            ...생략
        }
    }

    위 컨트랙트는 유니스왑의 factory contract 이다. 글의 목적이 factory 컨트랙트 코드분석이 아니기 때문에 함수 중간중간 생략한 부분이 있다는점을 참고하고 봐주시면된다. 누락된 함수는 없고 그대로 가져왔다. 

     

    대략적으로 역할을 보자면 가장중요한것은 createPool 함수와 getPool 매핑인것 같다. 풀을 생성하고 조회하는 것이 가장 중요해보이며 그외에도 오너를 설정할수 있는 함수와 수량? tick ? 이부분에 따라서 수수료 설정하는 함수 이렇게 구성이 되어있는 컨트랙트인거 같다.

     

    또한 IUniswapV3Factory(인터페이스), UniswapV3PoolDeployer(컨트랙트), NoDelegateCall(abstract 컨트랙트) 이렇게 3가지 컨트랙트를 상속받아이루어져 있다. 

     

    이번글에서는 일반 컨트랙트에서 상속은 다루지 않을거고 interface를 상속받는 부분과 abstract contract를 상속 받는 부분을 보면서 비교해보고자한다.

     

    abstract contract 는 추상함수 뿐만 비추상함수를 가지고 있을수도 있고 다양한 변수들도 가지고 있을수 있다 그냥 일반 컨트랙트이지만 추상함수가 있을수 있는 컨트랙트라고 생각하면되고 단독으로 사용되는 컨트랙트의 용도가 아닌 컨트랙트를 만드는데 도움이 되는 느슨한 틀을 제공해주는 역할이라고 생각할수있다.

    abstract contract 가 컨트랙트를 만드는데 도움이 되는 느슨한 틀의 역할은 한다면 interface는 엄격한 틀의 역할을 한다.

    인터페이스 내부에는 컨트랙트나 추상컨트랙트와는 다르게 오직 추상화된 함수와 이벤트만 선언할수 있다. 여기서 추상함수와 비추상함수는 함수의 실행부({ })가 구현이 되었는가 안되어 있는가를 이야기한다. 

    그리고 이 두가지를 상속받은 컨트랙트는 선언된 추상함수를 반드시 포함하여 실행부를 완성시켜 놓아야 하기 때문에 큰 프로젝트에서 디버깅에 유용하며 중복제거 확작성 등의 이점이 있다.

     

    예시로 해당 컨트랙트 코드를 보자

    abstract contract NoDelegateCall {
        address private immutable original;
    
        constructor() {
            original = address(this);
        }
    
        function checkNotDelegateCall() private view {
            require(address(this) == original);
        }
    
        modifier noDelegateCall() {
            checkNotDelegateCall();
            _;
        }
    }

    abstract contract 예시인데 특이한점은 추상함수가 없다. 그래서 되게 의아하게 느껴졌던부분이였다 왜 굳이 abstract 키워드를 사용했을까? 나를 헤깔리게 하려고 그러는건가? 이건 아니고 음... 명확하게 어떤 이유다 이런글은 본적은 없는데 아마도 nodelegatecall 컨트랙트가 단독으로 사용되기보다는 다른 컨트랙트에서 상속받아 사용할수 있도록 관련부분을 모아놓은 특징이 있기 때문에 느슨한 틀의 역할을 한다라고 판단해서 abstract contract 로 선언한 부분 인거 같다. 보통의 경우는 아래와 같이 사용하는 예시가 많을것이다.

    abstract contract BaseContract {
      int public baseX;
      
      constructor() {
        baseX = 10;
      }
    
      // "virtual" must be used in an abstract class for a function whether it is implemented or not
      function setX() virtual public;
    }

    setX() 함수는 실행부가 없고 virtual keyword 가 붙어있다. 추상함수는 실행부가 필요없지만 virtual 키워드를 같이 붙여줌으로써 override 해줘야한다고 명시해 줘야 한다. 상속받아 사용하는 컨트랙트에서 해당함수는 반드시 override 해줘야 하며 이후에 상속시에 override 가능한 함수인 경우에도 virtual 키워드를 붙여줄수 있다.

     

    이번에는 인터페이스 예시를 보자

    interface IUniswapV3Factory {
        event OwnerChanged(address indexed oldOwner, address indexed newOwner);
        event PoolCreated(
            address indexed token0,
            address indexed token1,
            uint24 indexed fee,
            int24 tickSpacing,
            address pool
        );
        event FeeAmountEnabled(uint24 indexed fee, int24 indexed tickSpacing);
    
        function owner() external view returns (address);
        function feeAmountTickSpacing(uint24 fee) external view returns (int24);
        function getPool(address tokenA, address tokenB, uint24 fee) external view returns (address pool);
        function createPool(address tokenA, address tokenB, uint24 fee) external returns (address pool);
        function setOwner(address _owner) external;
        function enableFeeAmount(uint24 fee, int24 tickSpacing) external;
    }

     

    그리고 특이하다고 느꼇던 부분이 여기 인터페이스의 추상함수에는 virtual 키워드가 없다 나는 virtual 키워드가 있어야 상속받은 컨트랙트에서 override 할수 있다라고 알고 있었는데 없어서 되게 의아하게 느껴졌던 부분이다. 근데 interface 에서는 다 추상함수여서 virtual을 명시하지 않아도 암묵적으로 그렇게 동작한다고 한다.

     

    아 그리고 Iuniswap~ 인터페이스에는 getPool 함수가 있는데 상속받은 factory에는 getPool 함수는 없고 getPool 매핑만 존재하는데 이거는 공식문서에서 getter function 부분을 보면 public 상태변수의 경우 자동으로 getter 함수를 생성해준다고 적혀있다.

    mapping(uint24 => int24) public override feeAmountTickSpacing;
    mapping(address => mapping(address => mapping(uint24 => address))) public override getPool;

    위 코드는 factory 컨트랙트의 일부인데 public 변수이기 때문에 getter 함수가 생성됬구나 라고 생각하면 되고 interface의 추상함수를 override 하는 부분이기 때문에 override 키워드가 같이 쓰여져 있는것을 확인할수 있다.

     

     

    참고:

    https://medium.com/upstate-interactive/solidity-how-to-know-when-to-use-abstract-contracts-vs-interfaces-874cab860c56

    https://coinsbench.com/smart-contract-basics-inheritance-abstraction-interface-718c2fba43b8

    https://goodgid.github.io/Ethereum-Basic-Solidity-(8) 

Designed by Tistory.