Hacking Ethereum Smart Contract Ponzi Schemes

Security is the most important consideration when we writing smart contracts. All is public in Ethereum, for example, etherscan which is the main source of checking source code from contracts deployed. Therefore, everything is public anyone can check source code, find issues and make exploits to steal big amount of money in critical cases.

Logical errors or some stuff forgotten when we write code can lead critical/high vulnerabilities, usually it translates to financial loses directly. Why not find some issues in ponzi smart contracts schemes and hacking the scammer for fun? First of all a tesnet localhost using brownie and ganache is needed. The security assessment was scoped to the smart contract provided in the following scope:

  • Chain Ponzi – (2) Critical/High
  • Tree Ponzi – (2) Critical/High

These smart contracts was developed by Alex Roan and based in this paper. Note: Some changes were done to get more impact and issues. Just for fun and learning, this post isn’t a realistic source code audit from business.

“A Ponzi scheme is a fraudulent investing scam promising high rates of return with little risk to investors. The Ponzi scheme generates returns for early investors by acquiring new investors. This is similar to a pyramid scheme in that both are based on using new investors’ funds to pay the earlier backers. Both Ponzi schemes and pyramid schemes eventually bottom out when the flood of new investors dries up and there isn’t enough money to go around. At that point, the schemes unravel.” — Investopedia

The pyramid Ponzi scheme is exactly what you’d expect: one owner at the top with, let’s say, 5 investors below him. Each investor is incentivised to get 5 more investors below them, and so on and so on. First example, I’m going to set up a scheme which “GUARANTEES TO DOUBLE YOUR MONEY” (until it can’t).

Technical review of chain ponzi

There are two critical/high issues in source code bellow. As the owner, a contract is deployed that allows anyone to join my DOUBLE YOUR MONEY GUARANTEED scheme. All I take is a small fee for every time someone invests, let’s say 10%.

  1. Minter invests 1 ether and earn 0.1 ether owner’s fees. Smart contract balance is 0.9 ether.
  2. User1 decides to invest 1 ether too. Minter takes fees and contract balance is 1.8 ether. No one has doubled their money yet, but I’ve made 0.2 ether for doing nothing.
  3. User2 comes in with another 1 ether, this is where it gets interesting. I take my fee, and now contract balance at 2.7 ether, enough to double minter’s original investment.
  4. And so on, and so on.

Several issues was found when source code audit is performed. One of them was a logic error. Since minter/scammer closed contract using close() function, investors could join and invest money and so on. Right now, balance in contract is frozen, so minter cannot withdraw money because contract boolean variable is set to false. Hence smart contract is closed, this cannot be used join function because balance of contract is frozen and never transfer to owner.

It is recommended to add the following line at the beginning of join() function.

require(active == true, "Contract must be active");

Therefore, if contract is closed and after that, join function is called error triggered.

In function join() can lead a denial of service when attacker use a smart contract to make such and exploit.
The Denial of Service (hence referred to as DoS) restricts legitimate users from using the smart contracts permanently or for a certain period unusable.

To explain how an Unexpected Revert can cause the DoS, consider the following attack smart contract.

When user2 (or random users) use join function (5) five times transfer to the attacker contract will fail. When balance of contract was higher than user amount*2, call() function is executed. This is because the Attacker contract has not implemented the receive() or fallback function to receive ether (We see after that the second PoC «Re-Entrancy «). Due to this any Solidity ether transfer function such as call(), send() or transfer() will result in an exception or unexpected revert due to the require() statement, stopping the execution.

In such a scenario, attacker contract will be the undisputed king and anyone can invest more money in ponzi scam. This contract is vulnerable to a reentrancy attack. But, can you discover the issue? The issue is the same as before due to NonRentrant implementation from OpenZeppelin library isn’t found from source code. Exploit contract is shown bellow:

  1. Since in Ethereum all is public, get source code from etherscan is possible. Attack function is called by user1 (attacker) and send 1 ether using join() function from Doubler contract.
  2. When user2 call five times join() function call is executed.
  3. Call back to attack.receive() performing multiple reentrants (if balance of contract is > 1 ether) by calling join() function.
  4. Once get balance in our contract, transfer is easy implemented a withdrawl function.

Therefore, we take advantage of the vulnerability performing multiple reentrants in order to earn more money for free!!.

Technical review of tree ponzi

To join the scheme, a user must send some money, and must indicate an inviter that will be his parent node. If the amount is too low, or if the user is already present, or if the inviter does not exist, the user is rejected; otherwise he is inserted in the tree. Once the user has joined, his investment is shared among his ancestors, halving the amount at each level.

In this scheme, a user cannot foresee how much he will gain: this depends on how many users he is able to invite, and on how much they will invest. The only one who is guaranteed to have profit is the owner/minter, i.e. the root node of the tree.

But there is a problem with the above explanation. Several issues was detected. First one is a logic error in the third require, which it checks that inviter has to exist. The developer may have confused with the require message displayed. Hence, it is not necessary that inviter exists in the tree, being able to put an arbitrary address account, since if it is an address that does not exist in the tree, ponzi scheme lacks sense. A malicious address account is used without the need of a referral/invitation before.

However, this issue would be solved quickly because when user1 invites user2, using the address account of user1 (remember that user1 is not in the tree!), when user2 wants to invite user3, he will not be able to because the statement below is executed, and he would be in the tree.

tree[msg.sender] = User(inviter, payable(msg.sender));

Finally, second issue is a consequence of the first one, in code’s line below. Since inviter didn’t check properly before in require statement if exist in tree, a transfer to zero address can occur losing inviter money doing ponzi scheme lacks sense. However, pretexting can be used by scammer sending broadcast message to all user saying or «promising» that money will be transfer and never occur.

current = tree[current].inviter; //vuln

current.transfer(amount);

Malicious user named user1 isn’t in tree, therefore does not exist in mapping variable and 0x0 address is present. The content of mapping is accessed and transaction is performed. Ether transaction is made using 0x0 address and money is lost. Meanwhile minter/owner or scammer always wins. The perfect damage scam using a pyramid scheme with vulnerabilities. However with these kinf of issues life of ponzi won’t last.

It is recommended use the code bellow:

But with this code above there is an informative issue which is the assumption who is inviter. All is public, and owner or user makes transfers in contract and we could build the tree using etherscan. If inviter in ponzi scheme is a public profile invitation cannot be a issue, but if invitation is considered a whitelisted or a privilege roles (the higher the position in the tree, the more privileges you have to use your address as an inviter) ,maybe is an issue because anyone could get an inviter address using etherscan and choose that from tree when use enter() function.

Unexpected DoS cannot perform because doesn’t exist a require statement after call(), send(), transfer() function. I hope it has been useful and any suggestions or vulnerabilities in source code would be appreciated said to me. Regards!.