Drawing conclusions from TON Hack Challenge
The TON Hack Challenge was held on October 23, 2022. Several smart contracts were deployed to TON Mainnet with synthetic security breaches. Every contract had a balance of 3000 or 5000 TON, allowing participants to hack them and get rewards immediately.
Source code and contest rules were hosted on GitHub in ton-blockchain/hack-challenge-1.
Contracts
1. Mutual fund
Always check functions for the impure
modifier.
The first task was very simple. The attacker could find that authorize
function was not impure
. The absence of this modifier allows the compiler to skip calls to that function if its result is unused.
() authorize (sender) inline {
throw_unless(187, equal_slice_bits(sender, addr1) | equal_slice_bits(sender, addr2));
}
2. Bank
Always check for modifying/non-modifying methods.
udict_delete_get?
was called with .
instead of ~
, so the real dict was untouched.
(_, slice old_balance_slice, int found?) = accounts.udict_delete_get?(256, sender);
3. DAO
Use signed integers only if you really need them.
Voting power was stored in the message as an integer. So the attacker could send a negative value during power transfer and get arbitrarily large voting power.
(cell,()) transfer_voting_power (cell votes, slice from, slice to, int amount) impure {
int from_votes = get_voting_power(votes, from);
int to_votes = get_voting_power(votes, to);
from_votes -= amount;
to_votes += amount;
;; No need to check that result from_votes is positive: set_voting_power will throw for negative votes
;; throw_unless(998, from_votes > 0);
votes~set_voting_power(from, from_votes);
votes~set_voting_power(to, to_votes);
return (votes,());
}
4. Lottery
Always randomize the seed before calling rand()
.
The seed was derived from the transaction's logical time, and a hacker can win by brute-forcing the logical time in the current block.
int seed = cur_lt();
int seed_size = min(in_msg_body.slice_bits(), 128);
if(in_msg_body.slice_bits() > 0) {
seed += in_msg_body~load_uint(seed_size);
}
set_seed(seed);
var balance = get_balance().pair_first();
if(balance > 5000 * 1000000000) {
;; forbid too large jackpot
raw_reserve( balance - 5000 * 1000000000, 0);
}
if(rand(10000) == 7777) { ...send reward... }
5. Wallet
Remember that everything is stored in the blockchain.
The wallet was protected with a password; its hash was stored in contract data. However, the blockchain remembers everything—the password was in the transaction history.
6. Vault
The vault has the following code in the database message handler:
int mode = null();
if (op == op_not_winner) {
mode = 64; ;; Refund remaining check-TONs
;; addr_hash corresponds to check requester
} else {
mode = 128; ;; Award the prize
;; addr_hash corresponds to the withdrawal address from the winning entry
}
The vault does not have a bounce handler or proxy message to the database if the user sends check. In the database, we can set msg_addr_none
as an award address because load_msg_address
allows it.
We request a check from the vault; the database tries to parse msg_addr_none
using parse_std_addr
and fails. The message bounces to the vault from the database, and op
is not op_not_winner
.
7. Better bank
Never destroy an account unnecessarily. Use raw_reserve
instead of sending money to yourself. Think about possible race conditions. Be careful with hashmap gas consumption.
There were race conditions in the contract: you could deposit money, then try to withdraw it twice in concurrent messages. There is no guarantee that a message with reserved money will be processed, so the bank can shut down after a second withdrawal. After that, the contract could be redeployed and anybody could withdraw unclaimed money.
8. Dehasher
Avoid executing third-party code in your contract.
slice try_execute(int image, (int -> slice) dehasher) asm "<{ TRY:<{ EXECUTE DEPTH 2 THROWIFNOT }>CATCH<{ 2DROP NULL }> }>CONT" "2 1 CALLXARGS";
slice safe_execute(int image, (int -> slice) dehasher) inline {
cell c4 = get_data();
slice preimage = try_execute(image, dehasher);
;; restore c4 if dehasher spoiled it
set_data(c4);
;; clean actions if dehasher spoiled them
set_c5(begin_cell().end_cell());
return preimage;
}
There is no way to safe execute a third-party code in the contract, because out of gas
exception cannot be handled by CATCH
. The attacker simply can COMMIT
any state of contract and raise out of gas
.
Conclusion
We hope this article sheds some light on the non-obvious rules for FunC developers.
References
- dvlkv on GitHub - Dan Volkov
- Original article - Dan Volkov