What FoMo3D’s real exit scam MIGHT look like (updated)

Update 8/22/2018 — Round 1 of FoMo3D:long has ended without any contract exploit. I still wish Team JUST would verify the abandoned FoMo3D (otherFoMo3D) contract. It now has over 1,000 ETH and continues to be an integral part of every key purchase.

It would also seem that when I wrote below “a song that likely will never stop”, it took just under 1 month for “never” to occur.

Update 8/13/2018 — The abandoned FoMo3D (otherFoMo3D) contract still has not been source verified.

Update 8/2/2018 — The abandoned FoMo3D (otherFoMo3D) contract has not been source verified, 8 days after it was expected to be.

Update 7/26/2018 — After discussing with Team JUST and researching more, I would like to significantly lower the threat outlined in the exploit “Vulnerability #1: Jekyll_Island_Inc”. In the article below, I noted the impact of wasting all gas inside the Jekyll_Island_Inc deposit call, but falsely claimed this would lead to complete transaction failure. Wasting all gas inside Jekyll_Island_Inc will NOT cause buys to fail. It will, however, cause the required gas to buy keys to increase from ~225K to >4M. The reason for this is quite interesting, but something for another article. (Related to EIP-114 and 1/64 of remaining gas)

Given that keys will still be available for purchase at this increased gas cost, doing so does not seem to clearly benefit the contract authors, I would classify this impact as low

The remaining issue “Vulnerability #2: otherFoMo3D” has not yet been source verified. Team JUST has announced plans to to verify source in the next 24 hours.

  • I am not claiming no other exploits exist.
  • I am not claiming that participating is a good idea.
  • I believe the impact of successfully executing #1 (Jekyll_Island_Inc) is low (block gas limits plummeting notwithstanding)
  • If you don’t see source verification of otherFoMo3D soon, ask why

“Team JUST” released the on-chain Ethereum game/dapp, “FoMo3Dlong” (dapp: https://exitscam.me) on July 6, 2018. My research partner, Micah Zoltu, sent me a message on July 20, 2018, pointing out that the contract exceeded 20,000 ether for a USD valuation of about $10M at the time. We decided to independently audit the contract for both vulnerabilities as well as the possibility that the contract author created a system that benefits themselves (in addition to the 2% fee they take on all incoming ether). I figure with a name like “exitscam.me”, no one should be surprised if the authors do.

A contract vulnerability has already been found, as highlighted in this reddit thread. (TL;DR~ be careful about random incentives and how you check for contract interactions)

While that vulnerability is widely known and currently being exploited by multiple parties, what Micah and I found in our investigation is potentially a more serious issue, one that could give the contract authors the ability to exit, at present time (block 6022481), for 10,338 Ether (~$5M USD). It also might be nothing; we have not discovered definitive proof that the attack is possible, only that all FoMo3Dlong buy() functions enter two different, source code unverified contracts that could potentially allow for a very profitable attack by the contract authors.

To understand the nature of this attack, it is important to understand what “winning” FoMo3Dlong entails. A player sends ether to the contract via one of the “buy” functions (such as “buyXaddr()”) to acquire “keys”. There’s a complex system around keys, payouts, and “Divies” that operates like a pyramid scheme, but those details are not important for this attack. What IS important is that buying at least one full “key” (currently: ~0.005 ether) makes you the current “winner”. However, the next person to buy at least one key (potentially/likely in the same block) becomes the “winner”, immediately removing you from your prior glory. Winning FoMo3Dlong consists of being the current “winner” when time runs out, granting you 48% of the “pot”. What makes winning difficult is each key purchased extends the timer by 30 seconds (up to a max of 24 hours). Team JUST has created a a penny auction for, currently, 10,338 Ether.

It’s a game of musical chairs with 10,000 people, one chair, a lot of money, and a song that likely will never stop.

One strategy for claiming this reward is to become the current “winner”, then break the contract for all future buys, ensuring no one else can become the winner while allowing the timer to expire (in no more than 24 hours). Team JUST currently has t̵w̵o̵ one potential way to accomplish this, as the main (verified) contract unsafely enters two unverified contracts. If this contract contains a function which causes future calls to revert or waste all incoming gas, Team JUST could trivially take down the jackpot.

I am not claiming that such a function exists, only that it is possible due to unverified Etherscan source code.


How a contract-breaking attack would work

Say Team JUST decides it is time to exit, that the amount gained from winning 48% of the pot far exceeds what they could hope to make on future endeavors. If they planned ahead, with contract functions to switch how their referenced contracts behave, here’s how they would do it:

The Setup

From any account, deploy this contract:

contract CircuitBreaker {
FoMo3D fomo3dlong = FoMo3D(0xA62142888ABa8370742bE823c1782D17A0389Da1);
FoMo3D killContract = FoMo3D(0xf9ba0955b0509ac6138908ccc50d5bd296e48d7d);
  function conditionalBreak() external {
if (fomo3dlong.round_(1).plyr == ATTACKING_FOMO_ID) {
// Function does not exist, we are speculating on the existence of a function like this
killContract.lockItDown();
}
}
}

Note: the above code does not work. The query for .plyr comes from a struct, this lookup will require assembly.

The Execution

With the above contract deployed, the attack would fire two transactions, from the same account, at the same time, at identical, high gas prices to make it unlikely to have other transactions between.

Transaction 1:
 — nonce: 0
 — gasPrice: 102.2 Gwei
 — to: FoMo3Dlong: buyXid() — purchase one key
Transaction 2:
 — nonce: 1
 — gasPrice: 102.2
 — to: CircuitBreaker.conditionalBreak()

By using the conditionalBreak() here, instead of directly locking down the contract without first checking who the winner is, we can take another shot at becoming the last winner in case the transactions are not ordered together.

The Wait

From here, simply wait out the timer, bask in all the failing buy transactions, then claim your reward once the timer expires

Vulnerability #1: Jekyll_Island_Inc

See update above, this attack is possible but its impact does not fully prevent key purchase

The FoMo3Dlong contract was already redeployed to address a prior complaint about the possibility of intentionally breaking the contract, allowing for the attack. Here is the most recent, patched code:

 // community rewards
if (!address(Jekyll_Island_Inc).call.value(_com)(bytes4(keccak256(“deposit()”))))
{
// This ensures Team Just cannot influence the outcome of FoMo3D with
// bank migrations by breaking outgoing transactions.
// Something we would never do. But that’s not the point.
// We spent 2000$ in eth re-deploying just to patch this, we hold the
// highest belief that everything we create should be trustless.
// Team JUST, The name you shouldn’t have to trust.
_p3d = _p3d.add(_com);
_com = 0;
}

This code is intended to deposit 2% of the incoming ETH into the Jekyll_Island_Inc contract, but proceed with the buy even if that deposit call reverts (“.call”). However, all gas is being forwarded to this call, which has the potential to waste all gas, preventing the rest of the function from proceeding. Jekyll_Island_Inc points to 0xdd4950F977EE28D2C132f1353D1595035Db444EE, which is a verified contract, but it simply forwards the “deposit()” call to another contract, 0x4c7b8591c50f4ad308d07d6294f2945e074420f5, which is not verified.

Additionally, it seems that there is a method in place on “Jekyll_Island_Inc” to simply swap where the deposit() call is forwarded to:

function startMigration(address _newCorpBank)
external
returns(bool)
{
// make sure this is coming from current corp bank
require(msg.sender == address(currentCorpBank_), "Forwarder startMigration failed - msg.sender must be current corp bank");

// communicate with the new corp bank and make sure it has the forwarder
// registered
if(JIincInterfaceForForwarder(_newCorpBank).migrationReceiver_setup() == true)
{
// save our new corp bank address
newCorpBank_ = _newCorpBank;

Even without premeditation, Team JUST very likely could swap out the “currentCorpBank” to a contract that conditionally wastes all gas on deposit(), enabling this attack. Further supporting this possibility is that “PUSH4 0xa0f52da0” appears in currentCorpBank’s opcodes, which maps to keccak256(“startMigration(address)”)

See update above, this attack is possible but its impact does not fully prevent key purchase

Vulnerability #2: otherFoMo3D

The marketing materials for FoMo3D note that 1% of incoming ether is being sent into a different, unverified FoMo3D (short/quick), supposedly seeding the pot to help kick off the future action (much like antes or blinds in a game of Poker). The code that supports that is here:

otherFoMo3D private otherF3D_;
… SNIP …
// pay 1% out to FoMo3D short
uint256 _long = _eth / 100;
otherF3D_.potSwap.value(_long)();

That potSwap invocation occurs as a part of every buy function (by way of buyXaddr() -> buyCore() -> core() -> distributeExternal) and is NOT invoked using the “.call” method: any revert inside otherF3D_.potSwap() will cause a revert of the buy() function. Additionally, simply wasting all gas such that none is left to resume the buy function would work as well. I also find it a questionable/sketchy decision to make the “otherF3D_” variable private. It is set to “0xf9ba0955b0509ac6138908ccc50d5bd296e48d7d” and cannot be changed.

Conclusion

This exploit relies on unverified contracts having functionality in them to enable this attack. The required features may not exist in deployed contracts, and there may not be any way for Team JUST to take advantage of the behaviors inside FoMo3Dlong. T̶h̶a̶t̶ ̶s̶a̶i̶d̶,̶ ̶i̶t̶ ̶s̶e̶e̶m̶s̶ ̶e̶x̶t̶r̶e̶m̶e̶l̶y̶ ̶l̶i̶k̶e̶l̶y̶ ̶t̶h̶e̶y̶ ̶c̶o̶u̶l̶d̶ ̶s̶w̶a̶p̶ ̶o̶u̶t̶ ̶t̶h̶e̶i̶r̶ ̶”̶b̶a̶n̶k̶”̶ ̶c̶o̶n̶t̶r̶a̶c̶t̶,̶ ̶g̶i̶v̶e̶n̶ ̶t̶h̶a̶t̶ ̶f̶u̶n̶c̶t̶i̶o̶n̶a̶l̶i̶t̶y̶ ̶i̶s̶ ̶c̶o̶d̶e̶d̶ ̶i̶n̶t̶o̶ ̶t̶h̶e̶ ̶J̶e̶k̶y̶l̶l̶_̶I̶s̶l̶a̶n̶d̶_̶I̶n̶c̶ ̶i̶n̶t̶e̶r̶m̶e̶d̶i̶a̶r̶y̶ ̶c̶o̶n̶t̶r̶a̶c̶t̶

We are dealing with unverified source code here, which is susceptible BOTH to malicious intent and bugs, neither of which we can see without source. While a contract that has been source code verified on Etherscan is a good start, it is not fully source verified until all contracts it calls are also source code verified (or interactions with those contracts account properly for failure). Team JUST has one unverified contract that hasthe potential to shutdown FoMo3Dlong, allowing a single player to claim the jackpot for themselves. This issue can be fully resolved by Team JUST uploading the contract source for the following contracts:

G̶i̶v̶e̶n̶ ̶t̶h̶e̶ ̶f̶u̶n̶c̶t̶i̶o̶n̶a̶l̶i̶t̶y̶ ̶o̶f̶ ̶”̶c̶u̶r̶r̶e̶n̶t̶C̶o̶r̶p̶B̶a̶n̶k̶_̶”̶,̶ ̶i̶t̶ ̶m̶a̶y̶ ̶a̶l̶s̶o̶ ̶r̶e̶q̶u̶i̶r̶e̶ ̶c̶h̶a̶n̶g̶i̶n̶g̶ ̶t̶o̶ ̶a̶ ̶b̶a̶n̶k̶ ̶t̶h̶a̶t̶ ̶i̶s̶ ̶b̶o̶t̶h̶ ̶v̶e̶r̶i̶f̶i̶e̶d̶ ̶a̶n̶d̶ ̶d̶o̶e̶s̶ ̶n̶o̶t̶ ̶a̶l̶l̶o̶w̶ ̶”̶s̶t̶a̶r̶t̶M̶i̶g̶r̶a̶t̶i̶o̶n̶”̶ ̶t̶o̶ ̶b̶e̶ ̶c̶a̶l̶l̶e̶d̶ ̶a̶g̶a̶i̶n̶.̶