OneSwap Series 11 — Security Verification, Fool-proof Design, and Friction Prevention for ETH Contracts

Introduction

As we all know, the blockchain world follows such a principle: Code is law. Ethereum smart contracts developed on Solidity contain a series of storage states to support the functions of Dapp; during the services provided by Dapp, due to the absence of censorship and decentralization of the blockchain, any organization or individual can call it at will. At this time, to protect the contract to make it operate according to the predetermined logic, it is necessary to stay alert throughout the contract operation to avoid the state of the contract deviating from the predetermined track, which is one source of security hazards.

  1. Fool-proof design: honest but careless users may call the contract in a wrong way or call the wrong contract, incurring losses to themselves; a well-designed contract will try to avoid the user’s loss by stopping execution upon detecting a user’s mistake or ensuring some remedies even after he makes a mistake
  2. Friction: Just like an object is difficult to slide on a surface with a large friction coefficient, a smart contract cannot be executed if it encounters frequent abnormalities and reversions; what’s worse, friction will make a smart contract unable to serve users normally. Hackers may take advantage of the friction in a contract for attacks that harm others yet do themselves no good: they attempt to disturb the normal operation of your contract even they gain no benefits by doing so.

Raising Exceptions in Solidity

Ethereum provides three exception-raising mechanisms to check the parameters received by the contract and some intermediate states generated during the operation of the contract; by doing so, it can avoid a malicious user’s input destroying the persistent state inside the contract; it can also suspend contract execution in time when an intermediate state does not meet the predetermined demand to reduce the Gas consumption for the user.

contract DemoContract{  uint const TICKET = 10;
address pool; function addPositive(uint a, uint b) public payable returns(uint){

require(a > 0 && b > 0, "params are not Positive integer");

// error reason is optional
require(this.balance >= TICKET ); assert(pool.send(TICKET), "transfer eth failed");
}}
contract DemoRevert{
uint state;
function operation(uint num) public{
if (num > 0 && num < 10){

// some operation
...... if (num > 3){
revert("Invalid Calculate")
}
}
}

}

Security verification

The necessity of security verification is evident. There are so many hackers staring at on-chain contracts, so you need to assume they can call in various possible ways every function of the contract that can be called and try to see if there are vulnerabilities exposed to them. Therefore, the validity verification for input parameters and the verification of the user’s transaction amount are both essential and thus common.

require((amount >> 42) == 0, "OneSwap: INVALID_AMOUNT");
uint32 m = price32 & DecFloat32.MANTISSA_MASK;
require(DecFloat32.MIN_MANTISSA <= m && m <= DecFloat32.MAX_MANTISSA, "OneSwap: INVALID_PRICE");
function _checkRemainAmount(Context memory ctx, bool isBuy) private view {
ctx.reserveChanged = false;
uint diff;
if(isBuy) {
uint balance = _myBalance(ctx.moneyToken);
require(balance >= ctx.bookedMoney + ctx.reserveMoney, "OneSwap: MONEY_MISMATCH");
diff = balance - ctx.bookedMoney - ctx.reserveMoney;
if(ctx.remainAmount < diff) {
ctx.reserveMoney += (diff - ctx.remainAmount);
ctx.reserveChanged = true;
}
} else {
uint balance = _myBalance(ctx.stockToken);
require(balance >= ctx.bookedStock + ctx.reserveStock, "OneSwap: STOCK_MISMATCH");
diff = balance - ctx.bookedStock - ctx.reserveStock;
if(ctx.remainAmount < diff) {
ctx.reserveStock += (diff - ctx.remainAmount);
ctx.reserveChanged = true;
}
}
require(ctx.remainAmount <= diff, "OneSwap: DEPOSIT_NOT_ENOUGH");
}
function _removeLiquidity(address pair) private {
(address a, address b) = IOneSwapFactory(factory).getTokensFromPair(pair);
require(a != address(0) || b != address(0), "OneSwapBuyback: INVALID_PAIR");
......
}

Foolproof design

Similar to various physical tools in the real world (e.g. the two electrodes of the battery to prevent users from misplacement), contracts developed by Solidity also come with fool-proof design, such as preventing users from transferring ETH to a contract account by mistake or refunding the extra funds. Oneswap has many such fool-proof designs.

modifier onlyOwner() {
require(msg.sender == _owner, "OneSwapToken: MSG_SENDER_IS_NOT_OWNER");
_;
}
modifier onlyNewOwner() {
require(msg.sender == _newOwner, "OneSwapToken: MSG_SENDER_IS_NOT_NEW_OWNER");
_;
}
function changeOwner(address ownerToSet) public override onlyOwner {
require(ownerToSet != address(0), "OneSwapToken: INVALID_OWNER_ADDRESS");
require(ownerToSet != _owner, "OneSwapToken: NEW_OWNER_IS_THE_SAME_AS_CURRENT_OWNER");
require(ownerToSet != _newOwner, "OneSwapToken: NEW_OWNER_IS_THE_SAME_AS_CURRENT_NEW_OWNER");

_newOwner = ownerToSet;
}
function updateOwner() public override onlyNewOwner {
_owner = _newOwner;
emit OwnerChanged(_newOwner);
}

function limitOrder(bool isBuy, address pair, uint prevKey, uint price, uint32 id,
uint stockAmount, uint deadline) external payable override ensure(deadline) {

(address stock, address money) = _getTokensFromPair(pair);
{
(uint _stockAmount, uint _moneyAmount) = IOneSwapPair(pair).calcStockAndMoney(uint64(stockAmount), uint32(price));
if (isBuy) {
if (money != address(0)) { require(msg.value == 0, 'OneSwapRouter: NOT_ENTER_ETH_VALUE'); }
_safeTransferFrom(money, msg.sender, pair, _moneyAmount);
}
......
}
IOneSwapPair(pair).addLimitOrder(isBuy, msg.sender, uint64(stockAmount), uint32(price), id, uint72(prevKey));
}
function _safeTransferFrom(address token, address from, address to, uint value) internal {
if (token == address(0)) {
_safeTransferETH(to, value);
uint inputValue = msg.value;
if (inputValue > value) { _safeTransferETH(msg.sender, inputValue - value); }
return;
}
.....
}

Friction prevention

Not all unreasonable parameters in a contract must throw exceptions using require. Take the simplest example: although the transfer amount of ERC20 tokens is unreasonably 0, it will not have any impact on the contract or the user, so just return without any other actions; too many reverted executions may terminate the calling contract in a surprising way. After all, based on common sense, when the transfer amount of assets is 0, the state will not be changed at all. The "unprovoked termination of the calling contract" here is what we call "friction".

// safely transfer ERC20 tokens, or ETH (when token==0)
function _safeTransfer(address token, address to, uint value, address ones) internal {
if(value==0) {return;}
if(token==address(0)) {
// limit gas to 9000 to prevent gastoken attacks
// solhint-disable-next-line avoid-low-level-calls
to.call{value: value, gas: 9000}(new bytes(0)); //we ignore its return value purposely
return;
}
// solhint-disable-next-line avoid-low-level-calls
(bool success, bytes memory data) = token.call(abi.encodeWithSelector(_SELECTOR, to, value));
success = success && (data.length == 0 || abi.decode(data, (bool)));
if(!success) { // for failsafe
address onesOwner = IOneSwapToken(ones).owner();
// solhint-disable-next-line avoid-low-level-calls
(success, data) = token.call(abi.encodeWithSelector(_SELECTOR, onesOwner, value));
require(success && (data.length == 0 || abi.decode(data, (bool))), "OneSwap: TRANSFER_FAILED");
}
}
function removeLiquidity(address[] calldata pairs) external override {
for (uint256 i = 0; i < pairs.length; i++) {
_removeLiquidity(pairs[i]);
}
}
function _removeLiquidity(address pair) private {
....

uint256 amt = IERC20(pair).balanceOf(address(this));
if (amt == 0) { return; }
....
}

Summary

This article introduces three exception-throwing mechanisms, and use some real cases to describe how to perform security verification using them. Then it also elaborates on some common fool-proof designs that aim to protect users from possible asset losses due to misoperation. Finally, it introduces some examples of reducing call friction to minimize the blocking during contract execution.

CoinEx Ambassador