↑も参考になったのですがちょっとたりなかったのでQiitaで記事にしておきます。誰かの参考にならんことを。。
USDTは通常のERC20ではない。
こんな事言われても大半の人は「え?」って感じだと思うんですが実際にはそう。英語記事をググるとUSDTは特殊、みたいな記載が散見されます。
で、これに気づくまではTestNetでERC20ぽいコントラクト作って普通にエラー出ないよねってテストやって本番環境持ってきてるはず。
なので本番でエラーが出て、下手したら急ぎでリリースしなきゃという状況で死ぬほど焦っているはず。
ポイントだけ先に書きます。
要点だけ先に書きます。
これ読んでいる人は死ぬほど焦っている可能性があるので(俺がそうだった)
- approveが特殊処理しないとならない場合がある。(一度ゼロでapprove市内とならないことがある)
- transferがおかしいので、そこの処理でこけることがある。 (値を返さない)
- decimalは6
この三点が対応出来れば問題ない。
どういうことだってばよ?
USDT作った人にはこだわりが合ったんだと思う
approveについてはUSDTのコードに記載されているとおりで攻撃を嫌ったのだと思う。けどそのせいで他のコントラクトと変わってしまった。
transferの戻りがないのはなんでか知らん。gas代下げたかったのかな
decimalは、USDTの性質上大量に使われることも想定したかっただろうしETHの1e18は明らかに多いからまぁ減らしたかったことは分かる。
これはERC20に準拠しているので、ちゃんとdecimal調べてチェックしてれば動くはずなのだけど、おろそかにしてETHと同じのdecimal=18みたいなコード書いてると当然おかしくなります。
対策
approveして居る箇所のコードを修正する。
一度approveして、再度approveしようとするとUSDTのコントラクトはエラーを出します。
再approveしたいときは一度ゼロでapproveして、approveを解除、その後にまたapprove行うという処理を書けば動く。
transferして居る箇所についての対応
SafeERC20を使って、safeTransfer , safeTransferFrom を使うのが楽
細かく言うとtransferの戻りがUSDTだけ無いんですよね。。
なので通常のERC20と同じ対応をして居ると実行時にエラーが起きる。
コンパイルは通ってしまうので問題に気づきづらい。
症状としてはfrontでcontractの関数を実行しようとするとなんかエラーが起きる、みたいな症状になっているはずなので、もしそういうのが起きててあなたがUSDT使っているなら一度疑った方がよい。
decimal
これは単に桁数の話なので適当に直して下さい。
サンプルコード
簡単にサンプルコードを書くとこういう感じに修正が必要という事になる。
(特にコントラクトにUSDTは受け取ったけど、コントラクトからUSDTが引き出せないとかになるとビジネス的には死んでしまうので、transferとtransferFromを両方safeTransfer, safeTransferFromに書き直してmainnetでのテストは必ず行うこと。
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.24;
import "@chainlink/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
contract Sample is Ownable {
using SafeERC20 for IERC20; //Use safeERC20 for IERC20
AggregatorV3Interface internal priceFeed; //USDT使うようなときはpriceFeedを使う事が多いが不要であれば不要.
IERC20 public usdtContract; //USDTコントラクト
uint256 public priceInUsd = 0.01 * 1e8; // 0.01 USDT equivalent in USD, 1e8で8桁の小数点を表現しているのは外部のpricefeedがdecima 8 で来ることが多いため計算時に混乱が少ないため。1e6で計算してももちろん構わない
// コンストラクタで初期設定
constructor(address _priceFeed, address _usdtContract) {
priceFeed = AggregatorV3Interface(_priceFeed);
usdtContract = IERC20(_usdtContract);
}
// USD価格の設定 1e8 で8桁の小数点を表現
function setPriceInUsd(uint256 newPriceInUsd) public onlyOwner {
priceInUsd = newPriceInUsd;
}
// USDTでの購入処理
function buyWithUSDT(uint256 amount) public {
uint256 usdtAmount = amount * priceInUsd / 1e2; // 1e8 の数値を元にしているので1e2で割る
usdtContract.safeTransferFrom(msg.sender, address(this), usdtAmount);// USDTの転送が成功したかを確認
/*
ここに購入処理を書く
// emit PurchaseMade(msg.sender, amount, usdtAmount); // イベントの発行
*/
}
//contract内でapproveを設定する処理もおそらく必要になるはず
function tokenAllowAll(address allowee) public {
if (usdtContract.allowance(address(this), allowee) != type(uint256).max) {
usdtContract.safeApprove(allowee, type(uint256).max);
}
}
// USDTの引き出し
function withdrawUSDT(uint256 amount) public onlyOwner {
require(usdtContract.balanceOf(address(this)) >= amount, "ERR006"); // トークン残高が引き出し額以上であることを確認
usdtContract.safeTransfer(owner(), amount);
}
// コントラクトのUSDT残高を取得
function getUSDTBalance() public view returns (uint256) {
return usdtContract.balanceOf(address(this));
}
}
どうすればよかったのか?
testnetでテストしたいですよね..
USDTのコードが落ちているのでテストネットではそれを自前でデプロイして試しましょう。
とはいえ俺は気づいたときにそんな時間が無かったからmainnetで修正してdeployしてました。
次からはtestnetにUSDTのコントラクト上げて試しますわ。。