
related articles:
Rust smart contract development diary (1) Contract state data definition and method implementation
Rust Smart Contract Development Diary (2) Writing Rust Smart Contract Unit Tests
Rust smart contract development diary (4) Rust smart contract integer overflow
Rust smart contract development diary (5) denial of service attack
Rust smart contract development diary (5) denial of service attack
Denial of service attack, also known as DoS (Denial of Service) attack, this type of attack will make the smart contract unable to be used by users normally for a period of time (even permanently).
The currently known causes can be roughly divided into the following two categories:"max_total_prepaid_gas"Certain flaws in the contract logic. Such as a public function, its implementation does not take into account the computational complexity. When the user calls this function, the actual gas consumed will exceed the gas defined in the NEAR public chain genesis block configuration file (genesis_config.json)
: 300000000000000` (300TGas), causing the transaction to fail.
In some cases of cross-contract calls, the execution of a contract depends on the execution status of other external contracts. However, the execution of external contracts is not always reliable, so that the execution of this contract may be blocked by external contracts and cannot operate as usual. The occurrence of this type of problem can be manifested as the contract user's funds in the contract are locked, so that it cannot be recharged or withdrawn normally.
In order to facilitate readers to have a deeper understanding of DoS attack vulnerabilities in smart contracts, this article will describe and analyze specific DoS attack examples later. The code of this article has been uploaded to BlockSec official github, readers can download https://github.com/blocksecteam/near_demo/tree/main/DoSDemo
secondary title
1. Loop over a data structure that can be changed by an external call
The following is a simple smart contract for "dividends" to registered users in the contract, and its state data is as follows:
Users can register and initialize by calling the pub fn register_account() function."Subsequently, the manager of the contract will call the pub fn distribute_token function to carry out the distribution for the users in the system"dividends
. The way of "dividend" is to traverse the user array self.registered, and transfer the specified amount of tokens to each user through cross-contract calls as rewards.
However, the size of the contract state data (self.registered) is not limited, and can be manipulated by malicious users, making the size of the contract data too large. So that when DISTRIBUTOR users call this contract method, the Gas fee that may be consumed is too high, exceeding the GAS LIMIT.
The following is the test result of the contract in the actual NEAR Localnet
It can be seen that when there are many registered users in the system, the prepaid_gas set will not be enough to satisfy the transfer operations of all users during the actual process of distribute_token execution, so that this transaction fails.
Recommended solution:
Therefore, it is recommended to use the withdrawal mode to transform the above contract. That is to say, the contracting party is required not to actively issue rewards to all users one by one, but to keep accounts first, and set up a withdraw function, so that a single user can withdraw the "dividend" reward by calling this function method. At this time, the contract party only needs to maintain the amount of rewards that have been withdrawn by each user or the amount of rewards that can still be retrieved.
secondary title
2. The state dependency between cross-contracts leads to contract blocking
When a contract makes a cross-contract call, it may depend on the state of the external contract. Improper dependence will cause the contract to block, which may cause a DoS attack
Let's consider a scenario where smart contracts are used for "bidding":
Users can register accounts by calling the pub fn register_account function method in the "bidding contract" to prepare for participating in subsequent bidding
Users can also query the user ID with the highest bid so far in the current system and the price they bid through the following interface function.
Users can also query the user ID with the highest bid so far in the current system and the price they bid through the following interface function.
When the bidding contract receives the token, it will call the following bid function through the ft_on_transfer function.
In this bidding function, the execution logic of the function will first check whether the current user's bid is higher than the previous highest bidder's bid value. If the condition is met, self.refund_exe() will be executed to return the bid tokens of the previous highest bidder from the "bidding contract". Then update the user ID with the highest bid so far and the price it bid.
The actual situation is that according to the logical definition of the contract: the bid tokens of the user with the highest bid must be returned in order to replace the ID of the user with the highest bid so far.
In this bidding function, the execution logic of the function will first check whether the current user's bid is higher than the previous highest bidder's bid value. If the condition is met, self.refund_exe() will be executed to return the bid tokens of the previous highest bidder from the "bidding contract". Then update the user ID with the highest bid so far and the price it bid.
The actual situation is that according to the logical definition of the contract: the bid tokens of the user with the highest bid must be returned in order to replace the ID of the user with the highest bid so far.
At this time, the test simulates the participating users of the "bidding system": user0, user1 and user2
They each have 10,000 initial tokens. user0 first bids 1000 in the "bidding system". At this time, the query shows that current_leader: user0.test.near highest_bid: 1000. Then user0 immediately transferred the remaining 9000 tokens to user2 and destroyed the token account."Cannot Refund"After that, when user1 bids 2000, the system will plan to return the previous bid value of user0. However, since the account of user0 no longer exists at this time, the system will prompt
, the subsequent transaction update status cannot be successfully completed.
At this point the second bidder wants to bid 2000:
If the state transformation of the contract depends on the call processing of the external contract, it is necessary to consider the possible failure of the external contract call to prevent the execution logic of the contract from being blocked and denial of service, that is, we need to implement reasonable error handling methods. In this example, we can deposit non-refundable tokens in the lost_found user group newly added to the contract. When subsequent users meet the refund conditions, the users themselves can further retrieve the tokens (the withdraw function can also be implemented) .
secondary title
3. Owner private key lost
Partial centralization often exists in decentralized smart contract projects: for example, there is a contract owner. The execution of some contract functions is set to be executable only by the owner, which is used to set and change the values of some key system variables in the contract. We can call such functions as only_owner type functions.
For example, for the pub fn distribute_token defined in the "dividend" contract above, this function is the only_owner function. When the owner of the contract cannot perform its functions (the private key is lost), the funds will always be locked in the contract and cannot be distributed to other users. In most cases, the only_owner function can also be used to suspend or restart all transactions in the contract, which shows the importance of the owner to perform its functions normally.
Solution: