[Code4rena] Tigris Trade

0xDave·2022년 12월 23일
0

Ethereum

목록 보기
79/112
post-thumbnail

Tigris is a leveraged trading platform that utilizes price data signed by oracles off-chain to provide atomic trades and real-time pair prices.


Scope


ContractSLOCPurposeLibraries used
contracts/Trading.sol794Contains most trading contract logic@openzeppelin/*
contracts/TradingExtension.sol195Some trading logic is delegated this contract@openzeppelin/*
contracts/utils/TradingLibrary.sol76Verifies oracle signature, calculates PnL and liquidation price. Checks against Chainlink's public price feeds.@openzeppelin/*
contracts/Position.sol219Position NFT that stores all position data@openzeppelin/*
contracts/PairsContract.sol106Stores info about pairs such as open interest and fees@openzeppelin/*
contracts/Referrals.sol56Stores referral codes and referred addresses@openzeppelin/*
contracts/GovNFT.sol263NFT that utilizes LayerZero for bridging and contains token reward distribution logic@openzeppelin/*
contracts/StableToken.sol46Mintable and burnable ERC20@openzeppelin/*
contracts/StableVault.sol66Holds liquidity for StableToken@openzeppelin/*
contracts/Lock.sol87Manages bond interaction logic for end-users@openzeppelin/*
contracts/BondNFT.sol284Bond NFTs minted by locking StableTokens and is managed by Lock.sol@openzeppelin/*
contracts/utils/MetaContext.sol27Context overridden for meta transactions@openzeppelin/*
contracts/interfaces/IBondNFT.sol36Bond interface
contracts/interfaces/IGovNFT.sol7Gov NFT interface
contracts/interfaces/ILayerZeroEndpoint.sol19LayerZero endpoint interface
contracts/interfaces/ILayerZeroReceiver.sol4LayerZero receiver interface
contracts/interfaces/ILayerZeroUserApplicationConfig.sol7LayerZero Config interface
contracts/interfaces/IPairsContract.sol22Pairs contract interface
contracts/interfaces/IPosition.sol48Position NFT interface
contracts/interfaces/IReferrals.sol7Referrals contract interface
contracts/interfaces/IStableVault.sol7StableVault interface
contracts/interfaces/IPosition.sol101ITrading interface

내가 생각한 결함


1. claim reentrancy 공격

//Lock.sol

    function claim(
        uint256 _id
    ) public returns (address) {
        claimGovFees();
        (uint _amount, address _tigAsset) = bondNFT.claim(_id, msg.sender);
        IERC20(_tigAsset).transfer(msg.sender, _amount);
        return _tigAsset;
    }
//BondNFT.sol

    function claim(
        uint _id,
        address _claimer
    ) public onlyManager() returns(uint amount, address tigAsset) {
        Bond memory bond = idToBond(_id);
        require(_claimer == bond.owner, "!owner");
        amount = bond.pending;
        tigAsset = bond.asset;
        unchecked {
            if (bond.expired) {
                uint _pendingDelta = (bond.shares * accRewardsPerShare[bond.asset][epoch[bond.asset]] / 1e18 - bondPaid[_id][bond.asset]) - (bond.shares * accRewardsPerShare[bond.asset][bond.expireEpoch-1] / 1e18 - bondPaid[_id][bond.asset]);
                if (totalShares[bond.asset] > 0) {
                    accRewardsPerShare[bond.asset][epoch[bond.asset]] += _pendingDelta*1e18/totalShares[bond.asset];
                }
            }
            bondPaid[_id][bond.asset] += amount;
        }
        IERC20(tigAsset).transfer(manager, amount);
        emit ClaimFees(tigAsset, amount, _claimer, _id);
    }

Tigris에 예치를 하면 BondNFT를 민팅해준다. 플랫폼에서 나온 fee를 NFT에 분배해주고, 예치한 사람은 이를 클레임하는 구조다. 이 때 자신이 지정한 기한 전에도 클레임이 가능하다. bond.pending 만큼만 클레임이 가능한데, bondPaid에만 클레임 신청한 금액을 추가하고 바로 transfer 해준다. bond.pending의 계산식에서 bondPaid를 차감하긴 하지만 이는 claim 함수 내부에 포함되어 있지 않다.

bond.pending = bond.shares * _accRewardsPerShare / 1e18 - bondPaid[_id][bond.asset];

따라서 만약 사용자가 의도적으로 Lock.sol의 claim 함수를 재호출하는 reentrancy 공격을 한다면 컨트랙트의 자금이 탈취 될 것이다.


2. Stable Token의 decimal 차이를 이용해 vault의 자금 탈취 가능

//StableVault.sol

    function deposit(address _token, uint256 _amount) public {
        require(allowed[_token], "Token not listed");
        IERC20(_token).transferFrom(_msgSender(), address(this), _amount);
        IERC20Mintable(stable).mintFor(
            _msgSender(),
            _amount*(10**(18-IERC20Mintable(_token).decimals()))
        );
    }
	
	//...

    function withdraw(address _token, uint256 _amount) external returns (uint256 _output) {
        IERC20Mintable(stable).burnFrom(_msgSender(), _amount);
        _output = _amount/10**(18-IERC20Mintable(_token).decimals());
        IERC20(_token).transfer(
            _msgSender(),
            _output
        );
    }

tigris 공식문서에 따르면 stable vault와 tigUSD를 1대1로 교환 가능하게 한다고 되어있다. 현재 지원 예정인 스테이블 토큰은 폴리곤 체인의 DAI와 아비트럼 체인의 USDT다. 만약 각 체인에서 decimal이 다른 스테이블 토큰을 추가 지원하거나 브릿지를 지원한다면 이를 이용한 공격이 발생할 수 있다. DAI의 decimal은 18이고 USDT의 decimal은 6이다. 이를 이용해서 deposit한 금액보다 더 많은 금액을 withdraw할 수 있다.

  1. USDT를 _amount 만큼 입금하고 tigUSD를 민팅한다.
  2. USDT의 decimal은 6이므로 tigUSD는 _amount*(10**12) 만큼 민팅된다.
  3. 민팅된 만큼 DAI를 withdraw 하면 DAI의 decimal은 18이므로
_amount*(10**12) / 10**0

만큼 인출할 수 있다. 따라서 decimal을 18로 하드코딩 하는것이 아니라 tigUSD의 decimal에 맞춰서 입금 및 출금이 가능하도록 해야 한다.


3. Reentrancy 공격을 통해 tigUSD가 계속 민팅될 수 있다.

//Trading.sol

    function cancelLimitOrder(
        uint256 _id,
        address _trader
    )
        external
    {
        _validateProxy(_trader);
        _checkOwner(_id, _trader);
        IPosition.Trade memory _trade = position.trades(_id);
        if (_trade.orderType == 0) revert();
        IStable(_trade.tigAsset).mintFor(_trader, _trade.margin);
        position.burn(_id);
        emit LimitCancelled(_id, _trader);
    }

해당 함수는 말 그대로 지정가 주문을 취소하는 함수다. 그런데 한 가지 눈에 띄는 점은 tigAsset을 먼저 민팅하고 포지션을 나중에 소각시킨다는 것이다. 악의적인 사용자가 컨트랙트를 이용해 포지션을 민팅한 다음, 취소할 때 Reentrancy 공격을 통해 tigUSD를 계속해서 민팅할 수 있다. 해결방법은

  1. 포지션을 먼저 소각시키고 나서 tigAsset을 민팅할 것.
  2. 함수에 ReentrancyGuard.sol을 통해 nonReentrant modifier를 적용할 것.

새로 배운 것


msg.sender == address(this)

//GovNFT.sol

    function _bridgeMint(address to, uint256 tokenId) public {
        require(msg.sender == address(this) || _msgSender() == owner(), "NotBridge");
        require(tokenId <= 10000, "BadID");
        for (uint i=0; i<assetsLength(); i++) {
            userPaid[to][assets[i]] += accRewardsPerNFT[assets[i]];
        }
        super._mint(to, tokenId);
    }

컨트랙트 자체가 msg.seder인 경우는 처음 봤다. 컨트랙트 내부에 _bridgeMint를 호출하는 함수를 하나 더 만들어서 민팅할 수 있는 구조로 되어있다. 이런 경우도 있구나 정도로 알고 넘어가면 될 것 같다.


MLOAD, MSTORE


harhat에서 하나의 파일만 테스트할 때

 npx hardhat test ./test/파일이름 (경로에 맞춰서 설정해줘야 한다.)

harhat에서 하나의 테스트 케이스만 테스트할 때

npx hardhat test --grep "테스트 케이스 이름"  

후기


사실 이번에는 각 케이스 별로 테스트 코드를 작성했었다. 그런데 allowance 에러가 발생하고, 내가 생각한대로 결과가 나오지 않았었다. 마감 기한이 있어서 결국 테스트 코드는 레포트에 첨부하지 못했다. 이번이 hardhat을 이용해서 처음으로 테스트 코드를 작성해봤는데 아직 많이 부족하다는 것을 알았다. 다음 컨테스트에도 계속 테스트 코드를 작성해보고 막히는 부분이 있으면 stackexchange에 질문도 해보면서 계속 시도해보자. foundry로 테스트해야 하는 프로젝트라도 docs 보면서 계속 해보자. 당장 눈에 보이는 성장이 없더라도 시간이 지나면 어느샌가 경험치가 쌓여있을 것이다.


출처 및 참고자료


  1. Solidity Assembly & ABI Encoding
profile
Just BUIDL :)

0개의 댓글