💵

【Solidity/web3.js】USDTを扱う際の注意点

2022/10/06に公開

以下2点の注意が必要みたいです。
久しぶりに実装で詰まったのでさくっと記事にしました。。

approve の実行

要点

allowed != 0の時に再度approveしたい時は、一度approve(_spender, 0)する必要がある。

繰り返しapprove(_spender, 10)などと実行することはできなく、間で必ずapprove(_spender, 0)を挟んだり、transferしてallowedを 0 にする必要がある。

詳細

USDTのapprove関数の中身は以下の通り。

https://etherscan.io/address/0xdAC17F958D2ee523a2206206994597C13D831ec7#code

/**
* @dev Approve the passed address to spend the specified amount of tokens on behalf of msg.sender.
* @param _spender The address which will spend the funds.
* @param _value The amount of tokens to be spent.
*/
function approve(address _spender, uint _value) public onlyPayloadSize(2 * 32) {

    // To change the approve amount you first have to reduce the addresses`
    //  allowance to zero by calling `approve(_spender, 0)` if it is not
    //  already 0 to mitigate the race condition described here:
    //  https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
    require(!((_value != 0) && (allowed[msg.sender][_spender] != 0)));

    allowed[msg.sender][_spender] = _value;
    Approval(msg.sender, _spender, _value);
}

requireのコメントに書いてある通りで、race conditionを軽減するためにapprove(_spender, 0)する必要がある。

具体的に発生しうる問題は以下の通り。

https://twitter.com/yagi_eng/status/1561664980021235712

requireの中身を読みやすくすると以下の通り。

require((_value == 0) || (allowed[msg.sender][_spender] == 0));

やりがちなミスと対策

ミス

  • front側で approve -> transferFrom を呼び出すコードを実装
  • ユーザがapproveだけで離脱してしまい、transferFrom が呼び出されなかった
  • 再度このユーザがapproveを呼び出そうとすると、allowed != 0のため、requireにはじかれる

対策

  • allowanceを呼び出して返り値 != 0の場合は、approve(_spender, 0)を呼び出す

Contractから呼び出す際はSafeERC20を使う

要点

Contract中でUSDTをtransferFromなどする際は、SafeERC20を使う必要がある

pragma solidity ^0.8.9;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

contract Test {
    using SafeERC20 for IERC20;

    function transfer(address _to, uint256 _amount) public {
        IERC20 token = IERC20(/* USDT address */);
        token.safeTransferFrom(msg.sender, _to, _amount);
    }
}

詳細

IERC20のtransferFromは返り値がbool

function transferFrom(
    address from,
    address to,
    uint256 amount
) external returns (bool);

USDTのtransferFromは返り値がない

// Forward ERC20 methods to upgraded contract if this one is deprecated
function transferFrom(
    address _from,
    address _to,
    uint256 _value
) public whenNotPaused {
    require(!isBlackListed[_from]);
    if (deprecated) {
        return
            UpgradedStandardToken(upgradedAddress).transferFromByLegacy(
                msg.sender,
                _from,
                _to,
                _value
            );
    } else {
        return super.transferFrom(_from, _to, _value);
    }
}

上記違いにより、IERC20をそのまま使うことはできないが、SafeERC20を挟むことで正常に呼び出せる。(浅い理解)

あるいは、USDTだけにフォーカスするのであれば、IERC20.solを使わずに自前でinterfaceを定義してもよさそう。

SafeERC20を使わないと。。

コンパイル・デプロイはできる。
SafeERC20を使っていない状態のContractの関数をweb3.js経由・Metamaskで呼び出そうとすると、警告が表示され、実行しても失敗する。

参考

https://forum.openzeppelin.com/t/can-not-call-the-function-approve-of-the-usdt-contract/2130

https://docs.openzeppelin.com/contracts/2.x/api/token/erc20#SafeERC20

Wrappers around ERC20 operations that throw on failure (when the token contract returns false). Tokens that return no value (and instead revert or throw on failure) are also supported, non-reverting calls are assumed to be successful. To use this library you can add a using SafeERC20 for ERC20; statement to your contract, which allows you to call the safe operations as token.safeTransfer(…​), etc.

さいごに

Twitterの方でも、モダンな技術習得やサービス開発の様子を発信したりしているので良かったらチェックしてみてください!

https://twitter.com/yagi_eng/status/1577976208926515200

また、個人開発したdAppsの解説記事もありますので、良かったらそちらもご覧ください!

https://twitter.com/yagi_eng/status/1503144502034563074

Discussion