체인의정석

폴리곤 브릿지 컨트렉트에서 ERC20 및 이더리움을 다루는 코드 분석 본문

블록체인/NFT & BRIDGE

폴리곤 브릿지 컨트렉트에서 ERC20 및 이더리움을 다루는 코드 분석

체인의정석 2022. 8. 8. 15:31
728x90
반응형

참고 : 폴리곤 pos portal 소스코드 중 RootChainManager.sol

 

폴리곤의 pos portal에서는 

delegate Call을 썼다. 주석을 보면 프록시 배포를 사용했기 때문에 사용했다고 하는 Call이 사용되고 있다.

하지만 프록시 배포를 사용하는 상황이 아니라면 이더리움을 직접 전송할 때는 transfer를 사용하는 것이 가장 좋다.

 

먼저 위는 일반 erc20 토큰을 예치할 때의 과정 아래는 이더리움과 같은 암호화폐를 예치할 때의 과정인데 결국 _deplositFor를 부르는 것은 동일하다. 여기서 넣는 ETHER ADDRSS의 경우 컨트렉트 상단에 명시되어 있는데

    address public constant ETHER_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;

이런식으로 되어 있다. 이건 주소 값을 임의로 정해 놓은 모양으로 보인다.

 

그리고 이렇게 임의로 정해놓은 주소값을 rootToken 자리에 넣어준다. 

 

원래 rootToken은 erc20 토큰 주소를 받아서 해당 주소를 tokenToType 이라는 구조체에서 주소를 넣으면 type이 나오고 그 type을 키 값으로 해서 CA값이 키 값으로 나오게 되면 해당 컨트렉트로 call을 통해 이더리움을 보내게 된다.

 

먼저 공개 되어 있는 함수의 경우 depositEtherFor를 사용한 후 external override payable을 사용하여서 외부에서 접근한 후 payable을 명시하여서 호출하고 있다. 

    /**
     * @notice Move ether from root to child chain, accepts ether transfer
     * Keep in mind this ether cannot be used to pay gas on child chain
     * Use Matic tokens deposited using plasma mechanism for that
     * @param user address of account that should receive WETH on child chain
     */
    function depositEtherFor(address user) external override payable {
        _depositEtherFor(user);
    }    
    
    /**
     * @notice Move tokens from root to child chain
     * @dev This mechanism supports arbitrary tokens as long as its predicate has been registered and the token is mapped
     * @param user address of account that should receive this deposit on child chain
     * @param rootToken address of token that is being deposited
     * @param depositData bytes data that is sent to predicate and child token contracts to handle deposit
     */
    function depositFor(
        address user,
        address rootToken,
        bytes calldata depositData
    ) external override {
        require(
            rootToken != ETHER_ADDRESS,
            "RootChainManager: INVALID_ROOT_TOKEN"
        );
        _depositFor(user, rootToken, depositData);
    }

    function _depositEtherFor(address user) private {
        bytes memory depositData = abi.encode(msg.value);
        _depositFor(user, ETHER_ADDRESS, depositData);

        // payable(typeToPredicate[tokenToType[ETHER_ADDRESS]]).transfer(msg.value);
        // transfer doesn't work as expected when receiving contract is proxified so using call
        (bool success, /* bytes memory data */) = typeToPredicate[tokenToType[ETHER_ADDRESS]].call{value: msg.value}("");
        if (!success) {
            revert("RootChainManager: ETHER_TRANSFER_FAILED");
        }
    }

그래서 결국 

1. 프록시 배포를 사용한 후 call 사용

2. 프록시 배포를 사용안 할 시에는 tranfer 사용

이렇게 하면 되는것 같다.

 

아무튼 모든 종류의 토큰(이더리움도 마찬가지)은 _depositFor을 통해서 TypeToPredicate를 통해 미리 등록해둔 타입별 컨트렉트 주소로 보내버린다.

    function _depositFor(
        address user,
        address rootToken,
        bytes memory depositData
    ) private {
        bytes32 tokenType = tokenToType[rootToken];
        require(
            rootToChildToken[rootToken] != address(0x0) &&
               tokenType != 0,
            "RootChainManager: TOKEN_NOT_MAPPED"
        );
        address predicateAddress = typeToPredicate[tokenType];
        require(
            predicateAddress != address(0),
            "RootChainManager: INVALID_TOKEN_TYPE"
        );
        require(
            user != address(0),
            "RootChainManager: INVALID_USER"
        );

        ITokenPredicate(predicateAddress).lockTokens(
            _msgSender(),
            user,
            rootToken,
            depositData
        );
        bytes memory syncData = abi.encode(user, rootToken, depositData);
        _stateSender.syncState(
            childChainManagerAddress,
            abi.encode(DEPOSIT, syncData)
        );
    }

 

 

여기서 보내지는 토큰의 종류로는

이와 같이 ERC20, 721, 1155 그리고 네이티브 토큰(가스 토큰) 등이 있다.

이러한 컨트렉트들은 직접 토큰을 보관하는 금고 역할을 하는 컨트렉트기 때문에 safeTrasferFrom을 사용해야 하며, 

 

한번 ERC20 과 Ether를 살펴보도록 하겠다.

 

ERC20 Predicate

contract ERC20Predicate is ITokenPredicate, AccessControlMixin, Initializable {
    using RLPReader for bytes;
    using RLPReader for RLPReader.RLPItem;
    using SafeERC20 for IERC20;

    bytes32 public constant MANAGER_ROLE = keccak256("MANAGER_ROLE");
    bytes32 public constant TOKEN_TYPE = keccak256("ERC20");
    bytes32 public constant TRANSFER_EVENT_SIG = 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef;

    event LockedERC20(
        address indexed depositor,
        address indexed depositReceiver,
        address indexed rootToken,
        uint256 amount
    );

    constructor() public {}

    function initialize(address _owner) external initializer {
        _setupContractId("ERC20Predicate");
        _setupRole(DEFAULT_ADMIN_ROLE, _owner);
        _setupRole(MANAGER_ROLE, _owner);
    }

    /**
     * @notice Lock ERC20 tokens for deposit, callable only by manager
     * @param depositor Address who wants to deposit tokens
     * @param depositReceiver Address (address) who wants to receive tokens on child chain
     * @param rootToken Token which gets deposited
     * @param depositData ABI encoded amount
     */
    function lockTokens(
        address depositor,
        address depositReceiver,
        address rootToken,
        bytes calldata depositData
    )
        external
        override
        only(MANAGER_ROLE)
    {
        uint256 amount = abi.decode(depositData, (uint256));
        emit LockedERC20(depositor, depositReceiver, rootToken, amount);
        IERC20(rootToken).safeTransferFrom(depositor, address(this), amount);
    }

    /**
     * @notice Validates log signature, from and to address
     * then sends the correct amount to withdrawer
     * callable only by manager
     * @param rootToken Token which gets withdrawn
     * @param log Valid ERC20 burn log from child chain
     */
    function exitTokens(
        address,
        address rootToken,
        bytes memory log
    )
        public
        override
        only(MANAGER_ROLE)
    {
        RLPReader.RLPItem[] memory logRLPList = log.toRlpItem().toList();
        RLPReader.RLPItem[] memory logTopicRLPList = logRLPList[1].toList(); // topics

        require(
            bytes32(logTopicRLPList[0].toUint()) == TRANSFER_EVENT_SIG, // topic0 is event sig
            "ERC20Predicate: INVALID_SIGNATURE"
        );

        address withdrawer = address(logTopicRLPList[1].toUint()); // topic1 is from address

        require(
            address(logTopicRLPList[2].toUint()) == address(0), // topic2 is to address
            "ERC20Predicate: INVALID_RECEIVER"
        );

        IERC20(rootToken).safeTransfer(
            withdrawer,
            logRLPList[2].toUint() // log data field
        );
    }
}

일단 Lock을 보니 encode를 해둔 데이터를 decode 하여 숫자만 받아와서 해당 수량 만큼 예치해 둔 것을 볼 수 있었고 exitToken 에서는 withdrawer 와 받는 양 만큼을 받아와서 보내주는 것을 볼 수 있다. 이때 ERC20 burn log를 가져와서 타당한지 확인하는데  이렇게 로그를 가져와서 검사하는 로직은 자체적인 브릿지에서의 검증 로직에서 불필요할 경우 생략해도 되므로 패스하였다.

 

 

Ether predicate

// File: contracts/root/TokenPredicates/EtherPredicate.sol

pragma solidity 0.6.6;

contract EtherPredicate is ITokenPredicate, AccessControlMixin, Initializable {
    using RLPReader for bytes;
    using RLPReader for RLPReader.RLPItem;

    bytes32 public constant MANAGER_ROLE = keccak256("MANAGER_ROLE");
    bytes32 public constant TOKEN_TYPE = keccak256("Ether");
    bytes32 public constant TRANSFER_EVENT_SIG = 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef;

    event LockedEther(
        address indexed depositor,
        address indexed depositReceiver,
        uint256 amount
    );

    event ExitedEther(
        address indexed exitor,
        uint256 amount
    );

    constructor() public {}

    function initialize(address _owner) external initializer {
        _setupContractId("EtherPredicate");
        _setupRole(DEFAULT_ADMIN_ROLE, _owner);
        _setupRole(MANAGER_ROLE, _owner);
    }

    /**
     * @notice Receive Ether to lock for deposit, callable only by manager
     */
    receive() external payable only(MANAGER_ROLE) {}

    /**
     * @notice handle ether lock, callable only by manager
     * @param depositor Address who wants to deposit tokens
     * @param depositReceiver Address (address) who wants to receive tokens on child chain
     * @param depositData ABI encoded amount
     */
    function lockTokens(
        address depositor,
        address depositReceiver,
        address,
        bytes calldata depositData
    )
        external
        override
        only(MANAGER_ROLE)
    {
        uint256 amount = abi.decode(depositData, (uint256));
        emit LockedEther(depositor, depositReceiver, amount);
    }

    /**
     * @notice Validates log signature, from and to address
     * then sends the correct amount to withdrawer
     * callable only by manager
     * @param log Valid ERC20 burn log from child chain
     */
    function exitTokens(
        address,
        address,
        bytes memory log
    )
        public
        override
        only(MANAGER_ROLE)
    {
        RLPReader.RLPItem[] memory logRLPList = log.toRlpItem().toList();
        RLPReader.RLPItem[] memory logTopicRLPList = logRLPList[1].toList(); // topics

        require(
            bytes32(logTopicRLPList[0].toUint()) == TRANSFER_EVENT_SIG, // topic0 is event sig
            "EtherPredicate: INVALID_SIGNATURE"
        );

        address withdrawer = address(logTopicRLPList[1].toUint()); // topic1 is from address

        require(
            address(logTopicRLPList[2].toUint()) == address(0), // topic2 is to address
            "EtherPredicate: INVALID_RECEIVER"
        );

        emit ExitedEther(withdrawer, logRLPList[2].toUint());

        (bool success, /* bytes memory data */) = withdrawer.call{value: logRLPList[2].toUint()}("");
        if (!success) {
            revert("EtherPredicate: ETHER_TRANSFER_FAILED");
        }
    }
}

Ether의 경우 다른 것은 크게 없었지만 , exited Ether 에서 withdrawer가 call을 해서 이더리움을 보내는 부분이 달랐다.

 

 

728x90
반응형
Comments