Tigris is a leveraged trading platform that utilizes price data signed by oracles off-chain to provide atomic trades and real-time pair prices.
Contract | SLOC | Purpose | Libraries used |
---|---|---|---|
contracts/Trading.sol | 794 | Contains most trading contract logic | @openzeppelin/* |
contracts/TradingExtension.sol | 195 | Some trading logic is delegated this contract | @openzeppelin/* |
contracts/utils/TradingLibrary.sol | 76 | Verifies oracle signature, calculates PnL and liquidation price. Checks against Chainlink's public price feeds. | @openzeppelin/* |
contracts/Position.sol | 219 | Position NFT that stores all position data | @openzeppelin/* |
contracts/PairsContract.sol | 106 | Stores info about pairs such as open interest and fees | @openzeppelin/* |
contracts/Referrals.sol | 56 | Stores referral codes and referred addresses | @openzeppelin/* |
contracts/GovNFT.sol | 263 | NFT that utilizes LayerZero for bridging and contains token reward distribution logic | @openzeppelin/* |
contracts/StableToken.sol | 46 | Mintable and burnable ERC20 | @openzeppelin/* |
contracts/StableVault.sol | 66 | Holds liquidity for StableToken | @openzeppelin/* |
contracts/Lock.sol | 87 | Manages bond interaction logic for end-users | @openzeppelin/* |
contracts/BondNFT.sol | 284 | Bond NFTs minted by locking StableTokens and is managed by Lock.sol | @openzeppelin/* |
contracts/utils/MetaContext.sol | 27 | Context overridden for meta transactions | @openzeppelin/* |
contracts/interfaces/IBondNFT.sol | 36 | Bond interface | |
contracts/interfaces/IGovNFT.sol | 7 | Gov NFT interface | |
contracts/interfaces/ILayerZeroEndpoint.sol | 19 | LayerZero endpoint interface | |
contracts/interfaces/ILayerZeroReceiver.sol | 4 | LayerZero receiver interface | |
contracts/interfaces/ILayerZeroUserApplicationConfig.sol | 7 | LayerZero Config interface | |
contracts/interfaces/IPairsContract.sol | 22 | Pairs contract interface | |
contracts/interfaces/IPosition.sol | 48 | Position NFT interface | |
contracts/interfaces/IReferrals.sol | 7 | Referrals contract interface | |
contracts/interfaces/IStableVault.sol | 7 | StableVault interface | |
contracts/interfaces/IPosition.sol | 101 | ITrading interface |
//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 공격을 한다면 컨트랙트의 자금이 탈취 될 것이다.
//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
할 수 있다.
_amount
만큼 입금하고 tigUSD를 민팅한다._amount
*(10**12) 만큼 민팅된다._amount*(10**12) / 10**0
만큼 인출할 수 있다. 따라서 decimal을 18로 하드코딩 하는것이 아니라 tigUSD의 decimal에 맞춰서 입금 및 출금이 가능하도록 해야 한다.
//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를 계속해서 민팅할 수 있다. 해결방법은
tigAsset
을 민팅할 것.nonReentrant
modifier를 적용할 것.//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
를 호출하는 함수를 하나 더 만들어서 민팅할 수 있는 구조로 되어있다. 이런 경우도 있구나 정도로 알고 넘어가면 될 것 같다.
npx hardhat test ./test/파일이름 (경로에 맞춰서 설정해줘야 한다.)
npx hardhat test --grep "테스트 케이스 이름"
사실 이번에는 각 케이스 별로 테스트 코드를 작성했었다. 그런데 allowance 에러가 발생하고, 내가 생각한대로 결과가 나오지 않았었다. 마감 기한이 있어서 결국 테스트 코드는 레포트에 첨부하지 못했다. 이번이 hardhat을 이용해서 처음으로 테스트 코드를 작성해봤는데 아직 많이 부족하다는 것을 알았다. 다음 컨테스트에도 계속 테스트 코드를 작성해보고 막히는 부분이 있으면 stackexchange에 질문도 해보면서 계속 시도해보자. foundry로 테스트해야 하는 프로젝트라도 docs 보면서 계속 해보자. 당장 눈에 보이는 성장이 없더라도 시간이 지나면 어느샌가 경험치가 쌓여있을 것이다.