[Code4rena] ParaSpace

0xDave·2022년 12월 2일
0

Ethereum

목록 보기
69/112
post-thumbnail

ParaSpace는 NFT 랜딩플랫폼이다. 백커로 세콰이어 등 유명 VC들이 있고 현재 ape 스테이킹으로 하잎을 가져가고 있는 프로젝트다. 자세한 설명은 블로그 글을 참고하자.


Scope


그 동안 읽었던 레포트 및 참가했던 콘테스트에 비해서 LOC와 Scope이 말도 안 되게 많다.. 하지만 상금도 그만큼 역대급이니 참가 안 할 이유가 없다.

ContractSLOCPurposeLibraries used
paraspace-core/contracts/misc/marketplaces/LooksRareAdapter.sol89Implements the NFT <=> ERC20 exchange logic via LooksRare marketplace@LooksRare/contracts-exchange/*
paraspace-core/contracts/misc/marketplaces/SeaportAdapter.sol112Implements the NFT <=> ERC20 exchange logic via OpenSea Seaport marketplace@ProjectOpenSea/seaport/*
paraspace-core/contracts/misc/marketplaces/X2Y2Adapter.sol92Implements the NFT <=> ERC20 exchange logic via X2Y2 marketplace@openzeppelin/*
paraspace-core/contracts/misc/NFTFloorOracle.sol337/// @notice Offchain clients can update the prices in this contract. The public can read prices@openzeppelin/*
paraspace-core/contracts/misc/ParaSpaceOracle.sol163Contract to get asset prices, manage price sources and update the fallback oracle@openzeppelin/*
paraspace-core/contracts/misc/UniswapV3OracleWrapper.sol307@Uniswap/v3-core/*
paraspace-core/contracts/protocol/configuration/PoolAddressesProvider.sol244Main registry of addresses part of or connected to the protocol, including permissioned roles@openzeppelin/*
paraspace-core/contracts/protocol/libraries/logic/AuctionLogic.sol114Implements actions involving NFT auctions@openzeppelin/*
paraspace-core/contracts/protocol/libraries/logic/BorrowLogic.sol159Implements the base logic for all the actions related to borrowing@openzeppelin/*
paraspace-core/contracts/protocol/libraries/logic/FlashClaimLogic.sol71@Chainlink/vrf/*
paraspace-core/contracts/protocol/libraries/logic/GenericLogic.sol442Implements protocol-level logic to calculate and validate the state of a user@openzeppelin/*
paraspace-core/contracts/protocol/libraries/logic/LiquidationLogic.sol645Implements actions involving management of collateral in the protocol, the main one being the liquidations@openzeppelin/*
paraspace-core/contracts/protocol/libraries/logic/MarketplaceLogic.sol482Implements the base logic for all the actions related to NFT buy/accept bid@openzeppelin/*
paraspace-core/contracts/protocol/libraries/logic/PoolLogic.sol175Implements the logic for Pool specific functions@openzeppelin/*
paraspace-core/contracts/protocol/libraries/logic/SupplyLogic.sol545Implements the base logic for supply/withdraw@openzeppelin/*
paraspace-core/contracts/protocol/libraries/logic/ValidationLogic.sol968Implements functions to validate the different actions of the protocol@openzeppelin/*
paraspace-core/contracts/protocol/pool/DefaultReserveAuctionStrategy.sol94Implements the calculation of the current dutch auction price@openzeppelin/*
paraspace-core/contracts/protocol/pool/PoolApeStaking.sol381ParaSpace Ape Staking Pool@openzeppelin/*
paraspace-core/contracts/protocol/pool/PoolCore.sol678Main point of interaction with an ParaSpace protocol's market@openzeppelin/*
paraspace-core/contracts/protocol/pool/PoolMarketplace.sol114Main point of interaction with an ParaSpace protocol's market@openzeppelin/*
paraspace-core/contracts/protocol/pool/PoolParameters.sol224Main point of interaction with an ParaSpace protocol's market@openzeppelin/*
paraspace-core/contracts/protocol/pool/PoolStorage.sol19Contract used as storage of the Pool contract.@openzeppelin/*
paraspace-core/contracts/protocol/tokenization/base/MintableIncentivizedERC721.sol434Basic ERC721 implementation@openzeppelin/*
paraspace-core/contracts/protocol/tokenization/NToken.sol267Implementation of the NFT derivative token for the ParaSpace protocol@openzeppelin/*
paraspace-core/contracts/protocol/tokenization/NTokenApeStaking.sol145Implementation of the NToken for the ParaSpace protocol@yoga-labs/ApeCoinStaking/*
paraspace-core/contracts/protocol/tokenization/NTokenBAYC.sol65Implementation of the NToken for the ParaSpace protocol@openzeppelin/*
paraspace-core/contracts/protocol/tokenization/NTokenMAYC.sol65Implementation of the NToken for the ParaSpace protocol@openzeppelin/*
paraspace-core/contracts/protocol/tokenization/NTokenMoonBirds.sol87Implementation of the interest bearing token for the ParaSpace protocol@openzeppelin/*
paraspace-core/contracts/protocol/tokenization/NTokenUniswapV3.sol109Implementation of the interest bearing token for the ParaSpace protocol@Uniswap/v3-core/*
paraspace-core/contracts/protocol/tokenization/libraries/MintableERC721Logic.sol427Implements the base logic for MintableERC721@openzeppelin/*
paraspace-core/contracts/protocol/tokenization/libraries/ApeStakingLogic.sol197Implements the base logic for ApeStaking@yoga-labs/ApeCoinStaking/*
paraspace-core/contracts/ui/WPunkGateway.sol156Implements the acceptBidWithCredit feature. AcceptBidWithCredit allows users to@openzeppelin/*

내가 생각했던 결함들


1. 이자율을 조작할 수 있다면?

//PoolCore.sol
    function borrow(
        address asset,
        uint256 amount,
        uint16 referralCode,
        address onBehalfOf
    ) external virtual override nonReentrant {
        DataTypes.PoolStorage storage ps = poolStorage();

        BorrowLogic.executeBorrow(
            ps._reserves,
            ps._reservesList,
            ps._usersConfig[onBehalfOf],
            DataTypes.ExecuteBorrowParams({
                asset: asset,
                user: msg.sender,
                onBehalfOf: onBehalfOf,
                amount: amount,
                referralCode: referralCode,
                releaseUnderlying: true,
                reservesCount: ps._reservesCount,
                oracle: ADDRESSES_PROVIDER.getPriceOracle(),
                priceOracleSentinel: ADDRESSES_PROVIDER.getPriceOracleSentinel()
            })
        );
    }

위 함수는 Pool에서 특정 자산을 담보로 토큰을 빌리는 기능을 한다. 그런데 코드에서 볼 수 있듯이 external로 선언되어 누구나 호출할 수 있다. 파라미터를 입맛대로 집어넣어서 다른 사람의 자산을 담보로 토큰을 빌릴 수 있다. 그러나 executeBorrow를 보면 debt token을 민팅하고 Interest rate가 조절된다.


//BorrowLogic.sol

 function executeBorrow(
        mapping(address => DataTypes.ReserveData) storage reservesData,
        mapping(uint256 => address) storage reservesList,
        DataTypes.UserConfigurationMap storage userConfig,
        DataTypes.ExecuteBorrowParams memory params
    ) public {
        DataTypes.ReserveData storage reserve = reservesData[params.asset];
        DataTypes.ReserveCache memory reserveCache = reserve.cache();

        reserve.updateState(reserveCache);

        ValidationLogic.validateBorrow(
            reservesData,
            reservesList,
            DataTypes.ValidateBorrowParams({
                reserveCache: reserveCache,
                userConfig: userConfig,
                asset: params.asset,
                userAddress: params.onBehalfOf,
                amount: params.amount,
                reservesCount: params.reservesCount,
                oracle: params.oracle,
                priceOracleSentinel: params.priceOracleSentinel
            })
        );

        bool isFirstBorrowing = false;

        (
            isFirstBorrowing,
            reserveCache.nextScaledVariableDebt
        ) = IVariableDebtToken(reserveCache.variableDebtTokenAddress).mint(
            params.user,
            params.onBehalfOf,
            params.amount,
            reserveCache.nextVariableBorrowIndex
        );

        if (isFirstBorrowing) {
            userConfig.setBorrowing(reserve.id, true);
        }

        reserve.updateInterestRates(
            reserveCache,
            params.asset,
            0,
            params.releaseUnderlying ? params.amount : 0
        );

        if (params.releaseUnderlying) {
            IPToken(reserveCache.xTokenAddress).transferUnderlyingTo(
                params.user,
                params.amount
            );
        }

        emit Borrow(
            params.asset,
            params.user,
            params.onBehalfOf,
            params.amount,
            reserve.currentVariableBorrowRate,
            params.referralCode
        );
    }

그런데 어차피 debt token은 부채를 관리하기위한 토큰으로 민팅과 소각만 가능하다. 다른 사람에게 보낼 수도 없기 때문에 온전히 관리 목적인 것으로 보인다. 여기서 주목할 만한 것은 Interest rate을 조절한다는 점이다. 이자율이 조절 가능하다는 것은 이를 이용해 자신의 포지션에 맞게 이자율을 유리하게 가져갈 수 있다는 것이다. 예상 시나리오는 다음과 같다.

  1. 해커는 ape 토큰을 예치하고 이를 통해 이자를 극대화하고 싶다.
  2. NFT를 예치한 주소를 파라미터로 넘겨서 debt token을 민팅하고 params.amount를 통해 이자율을 조작한다.
  3. 높아진 이자율을 바탕으로 해커가 원래 받아야할 이자보다 훨씬 많은 이자를 받는다.

2. NFT 가격을 조작할 수 있다?

//NFTFloorOracle.sol

    function setPrice(address _asset, uint256 _twap)
        public
        onlyRole(UPDATER_ROLE)
        onlyWhenAssetExisted(_asset)
        whenNotPaused(_asset) 
    {
        bool dataValidity = false;
        if (hasRole(DEFAULT_ADMIN_ROLE, msg.sender)) {
            _finalizePrice(_asset, _twap);
            return;
        }
        dataValidity = _checkValidity(_asset, _twap);
        require(dataValidity, "NFTOracle: invalid price data");
        // add price to raw feeder storage
        _addRawValue(_asset, _twap);
        uint256 medianPrice;
        // set twap price only when median value is valid
        (dataValidity, medianPrice) = _combine(_asset, _twap);
        if (dataValidity) {
            _finalizePrice(_asset, medianPrice);
        }
    }

    function _checkValidity(address _asset, uint256 _twap)
        internal
        view
        returns (bool)
    {
        require(_twap > 0, "NFTOracle: price should be more than 0");
        PriceInformation memory assetPriceMapEntry = assetPriceMap[_asset];
        uint256 _priorTwap = assetPriceMapEntry.twap;
        uint256 _updatedAt = assetPriceMapEntry.updatedAt;
        uint256 priceDeviation;
        //first price is always valid
        if (_priorTwap == 0 || _updatedAt == 0) {
            return true;
        }
        priceDeviation = _twap > _priorTwap
            ? (_twap * 100) / _priorTwap
            : (_priorTwap * 100) / _twap;

        if (priceDeviation >= config.maxPriceDeviation) {
            return false;
        }
        return true;
    }

setPrice 함수는 말 그대로 처음에 NFT 가격을 정하는 함수다. 이 때 _checkValidity를 통해 해당 가격이 유효한지 확인하는데, 특이한 건 처음 입력한 가격이라면 다 유효하다고 판단하는 거다. 여기까지는 문제될 것이 없다.


//NFTFloorOracle.sol

    function removeAsset(address _asset)
        external
        onlyRole(DEFAULT_ADMIN_ROLE)
        onlyWhenAssetExisted(_asset)
    {
        _removeAsset(_asset);
    }

    function _removeAsset(address _asset)
        internal
        onlyWhenAssetExisted(_asset)
    {
        uint8 assetIndex = assetFeederMap[_asset].index;
        delete assets[assetIndex];
        delete assetPriceMap[_asset];
        delete assetFeederMap[_asset];
        emit AssetRemoved(_asset);
    }

그러나 Asset을 지울 수 있다면 말이 달라진다. 물론 admin만 해당 함수를 호출할 수 있다. admin role이 탈취되지 않게 하는 것도 중요하지만 admin role이 탈취됐을 때 피해를 최소화하는 것도 중요하다. 또한 admin을 100% 신뢰할 수 없다. 만약 admin이 현재 풀 안에서 가격을 갖고 있는 asset을 삭제하고 곧 바로 완전 높은 가격을 설정하거나 완전 낮은 가격을 설정하면 대혼돈이 시작된다. 이 공격이 가능한 이유는 가장 처음 설정한 가격을 무조건 유효하다고 판단하기 때문이다.


후기


대략 3일을 온전히 콘테스트 참가하는 데 사용했다. 시간을 많이 쓰면서 든 생각들을 정리해보고자 한다.

  1. 조그만 결함보다는 큰 결함을 찾으려고 하자.

콘테스트 초반에 나도 모르게 이런 생각이 마음 속에 있었다. '조그만 결함이라도 발견하면 다 제출하자.' 그런데 후반부에 가서 다시 생각해보니 큰 오만이었다. 초반의 마인드는 나에게도 프로젝트에게도 전혀 도움이 되지 않는 마인드다.

솔직히 콘테스트에 걸려있는 상금이 크니까 다른 거 다 내팽개치고 여기에 대부분의 시간을 쏟아부은 것이 사실이다. 그런데 막상 찾은 결함이 프로젝트에 별 영향 없는 결함이라면? 조금은 도움이 되겠지만 내가 들인 시간에 비해 ROI가 현저히 낮아진다. 프로젝트 입장에서도 큰 상금을 건 이유는 프로젝트가 해커에게 피해입는 것을 방지하기 위함이다. 한 번 공격에 성공하면 그 규모가 매우 크기 때문에 사전에 큰 비용을 치뤄서라도 더 큰 피해를 막기 위해서다.

나는 조그만 결함이 아니라 프로젝트에 큰 영향을 끼칠 수 있는 결함을 발견하려고 해야 한다. 그래야 시간 당 ROI가 높아지고, 발견했을 때 프로젝트에게도 훨씬 많은 도움이 된다. 이왕 시간을 할애할 것이라면 해커에게 피해 받기 전 프로젝트라고 생각하고 중요한 결함을 발견하려고 해보자.


  1. 아님 말고 마인드 버리기.

'내가 발견한 결함을 제출했는데 만약 결함이 아니라면? 뭐 어쩔 수 없지.' 라는 안일한 생각으로 코드를 봤었다. 1번과 마찬가지로 이런 마인드는 나를 좀먹는 생각이다. 봤을 때 결함이라고 생각된다면 그걸 실제로 검증할 생각으로 이어져야 한다. 머리로만 '결함인 것 같은데?'라고 하지 말고 이미 직접 테스트 코드를 짜고있어야 한다. 테스트 코드를 못 짜겠으면 프로젝트 쪽에서 만들어 놓은 테스트 코드를 보고 배우자.


--
사실 후기 보다는 내가 자신한테 화가 나서 쓴 글인데 조금이라도 나중에 도움이 됐으면 해서 기록으로 남기고 있다. 이 글을 쓰고 있는 지금도 마음이 좋지 않다. 더 잘 하고 싶은데 부족한 게 눈에 보여서 더 그런 것 같다. 틈틈이 콘테스트에 참여하면서 과거 레포트도 계속 읽어나가는 방법 밖엔 없는 것 같다.


출처 및 참고자료


  1. NFT를 담보로 코인 대출받는 Paraspace 뭐하는곳인가?
profile
Just BUIDL :)

0개의 댓글