Taiyue alliance chain, security in smart contract

Call call

contract auction {
  address highestBidder;
  uint highestBid;
  function bid() {
    if (msg.value < highestBid) throw;
    if (highestBidder != 0)
      highestBidder.send(highestBid); // refund previous bidder
    highestBidder = msg.sender;
    highestBid = msg.value;
  }
}

Since the maximum stack depth is 1024, a new bidder can always increase the stack size to 1023 before calling bid(), which will result in a silent failure of the send(highestBid) call (i.e., the previous bidder will not be refunded), but the new bidder will still be the highest bidder. One way to check for a successful send is to check its return value:

if (highestBidder != 0)
  if (!highestBidder.send(highestBid))
    throw;

The only way to prevent these two situations is to change the sending mode to the withdrawing mode by letting the receiver control the transfer:

contract auction {
  address highestBidder;
  uint highestBid;
  mapping(address => uint) refunds;
  function bid() {
    if (msg.value < highestBid) throw;
    if (highestBidder != 0)
      refunds[highestBidder] += highestBid;
    highestBidder = msg.sender;
    highestBid = msg.value;
  }
  function withdrawRefund() {
    if (msg.sender.send(refunds[msg.sender]))
      refunds[msg.sender] = 0;
  }
}

Why do you still say "negative example" above the contract? For reasons of natural gas technology, the contract is actually possible, but it is still not a good example. The reason is that it is not possible to prevent code execution at the receiver as part of the send. This means that when the sending function is still in progress, the recipient can call back to withdrawredirect. At that time, the refund amount is still the same, so they will get a refund again, and so on. In this particular example, it doesn't work because the recipient only gets a gas allowance (2100gas) and can't perform another send with that amount of gas. The following code, though vulnerable to this attack:

msg.sender.call.value(refunds[msg.sender])()

The following code can solve

contract auction {
  address highestBidder;
  uint highestBid;
  mapping(address => uint) refunds;
  function bid() {
    if (msg.value < highestBid) throw;
    if (highestBidder != 0)
      refunds[highestBidder] += highestBid;
    highestBidder = msg.sender;
    highestBid = msg.value;
  }
  function withdrawRefund() {
    uint refund = refunds[msg.sender];
    refunds[msg.sender] = 0;
    if (!msg.sender.send(refund))
     refunds[msg.sender] = refund;
  }
}

Limitation of Gas

There is a limit to how much natural gas can be consumed in a block. The limit is flexible, but it's hard to increase it. This means that in all (reasonable) cases, each function in the contract should be kept below a certain amount of gas. The following are examples of bad voting contracts:

contract Voting {
  mapping(address => uint) voteWeight;
  address[] yesVotes;
  uint requiredWeight;
  address beneficiary;
  uint amount;
  function voteYes() { yesVotes.push(msg.sender); }
  function tallyVotes() {
    uint yesVotes;
    for (uint i = 0; i < yesVotes.length; ++i)
      yesVotes += voteWeight[yesVotes[i]];
    if (yesVotes > requiredWeight)
      beneficiary.send(amount);
  }
}

The contract actually has several problems, but what I want to emphasize here is the circular problem: assume that voting rights are transferable and divisible like tokens (take DAO tokens for example). This means that you can create any number of your own clones. Creating such a clone increases the length of the loop in the tallyVotes function until more gas is consumed than is available in a single block.

This applies to anything that uses a loop, as well as situations where the loop is not explicitly shown in the contract, such as when storing an internal copy array or string. Similarly, if the length of the loop is controlled by the caller, for example, if you traverse an array passed as a function parameter, you can use a loop of any length. However, do not create a situation where the loop length is controlled by one party, and one party is not the only one to suffer failure.

By the way, that's one of the reasons why we now have the concept of freezing accounts in DAO contracts: voting weight is calculated at the beginning of voting to prevent the cycle from getting into trouble and whether voting is fixed before the end of voting period. You can vote for a second time by only transferring tokens and then voting again.

Throw operation

row statements are usually very convenient, and any changes to the state (or the entire transaction, depending on how the function is called) can be recovered during a call. However, you have to know that it also causes all gas to be consumed, so it's expensive, and it's possible to stop calls to the current function. Therefore, I recommend using it only if:

1. send

If a function does not want to receive Ethernet currency in the current state or with the current parameters, throw rejection should be used. Because of the problem of gas and stack depth, using throw is the only way to send reliably: the receiver's fallback function may be wrong, which takes up too much gas, so it can't receive Ethernet, or this function may be a context with too high stack depth in malware (even before calling function).

2. Restore the effect of calling function

If you call functions on other contracts, you never know how they are implemented. This means that the effects of these calls are not known, so the only way to restore them is to use throw. Of course, if you know that you have to restore the effect, you should always write the contract without calling these functions in the first place, but in some cases, only after the fact.

Posted on Fri, 12 Jun 2020 05:22:43 -0400 by limao