//tips
//smart contract
echidna-testを実施するためにコントラクトを分離し、import部分を削除したtest用のコントラクトを作成。
docker run -it -v "$PWD":/home/training trailofbits/eth-security-toolbox
cd /home/training
cd docker
echidna-test test.sol
Ownable部分でエラー。
自分で整形しないと使いない可能性が高そうなので試しに下記を挿入。
contract Test {
event Flag(bool);
bool private flag0 = true;
bool private flag1 = true;
function set0(int val) public returns (bool){
if (val % 100 == 0)
flag0 = false;
}
function set1(int val) public returns (bool){
if (val % 10 == 0 && !flag0)
flag1 = false;
}
function echidna_alwaystrue() public returns (bool){
return(true);
}
function echidna_revert_always() public returns (bool){
revert();
}
function echidna_sometimesfalse() public returns (bool){
emit Flag(flag0);
emit Flag(flag1);
return(flag1);
}
}
このバグありのテストコードを実行。
echidna-test test.sol —Test
きちんとechidna-testで動くことが確認できた。上記の表記型を見るとわかるようにかなり特殊な条件でechidnaは使えるよう。基本的にreturnされるのはbool型のfunctionと変数で構成され、継承もimportも使わない場合にチェックできるよう。
もう少し複雑なコードを書くようになったら見直すか。
自身のコントラクト内容を見直す。
import "@openzeppelin/contracts/utils/Counters.sol”;について調べてみるとERC721のid管理に使われるもので、safemathと同様な効果があるよう。
Counters: a simple way to get a counter that can only be incremented or decremented. Very useful for ID generation, counting contract activity, among others.
https://docs.openzeppelin.com/contracts/3.x/api/utils#Counters
そもそものbaseuriとは何なのかも確認しておく。
function setBaseURI(string memory baseURI_) external onlyOwner {
ペンギンNFTではconstructorで設定されていたかと思うが、こちらはHPのurlにあたるものではなく、tokenidに基づいたmetaデータの格納先ipfsと関連しているのかを確認する。
NFTではmetadataをコードへのハードコーディングや変数に格納するのではなく、外部のjsonファイルなどに置き、そのURIを記録しておくことで、あとから変更したり、画像など膨大なデータを参照できるようにしている。
なので、BaseURI + token IDとしてipfsのuriを設定できるようにすることでコード量がかなり短縮される。
日本語での説明も見つけた。こちらがかなりわかりやすそう。
https://hanzochang.com/articles/5
string(abi.encodePacked(base, _tokenURI));にて文字列の連結を行う。
solidityでの文字列の連結はそのままではできない仕様なのでabi.encodePackedという技を使う必要がある。
ジェネラティブではなく単一の場合は、_tokenURIの運用のみで良いかもしれない。
自身のNFT生成時に下記を使ったか確認しておく。
function setBaseURI(string memory baseURI_) external onlyOwner {
_baseURIextended = baseURI_;
}
Counters.Counter private _tokenIds;としておくことで、 function claimItem(string memory _tokenURI) public returns (uint256) {にて
_tokenIds.increment();
を実行することができる。つまりidの数を1増やせる。
_safeMint(msg.sender, newItemId);をclaimの中で実行しているが、このsafemintは新たなNFTの創造という意味ではなく所有権の移転を安全にできるかのチェックを行い、_mintで所有権の移転をおこなっている。
function _safeMint(
address to,
uint256 tokenId,
bytes memory data
) internal virtual {
_mint(to, tokenId);
require(
_checkOnERC721Received(address(0), to, tokenId, data),
"ERC721: transfer to non ERC721Receiver implementer"
);
}
function _mint(address to, uint256 tokenId) internal virtual {
require(to != address(0), "ERC721: mint to the zero address");
require(!_exists(tokenId), "ERC721: token already minted");
_beforeTokenTransfer(address(0), to, tokenId);
_balances[to] += 1;
_owners[tokenId] = to;
emit Transfer(address(0), to, tokenId);
_afterTokenTransfer(address(0), to, tokenId);
}
オリジナル制作の場合ここはオーナーのみに絞った方が良さそう。
つまり最初の所有者は必ずownerとなるようにし、そこから所有権を移せるようにする。
このclaimを使用しないで所有権を移転させられることを確認しといた方がよさそう。これはテストネットで確認可能なので実験していく。
PudgyPenguins.solを確認してみると下記のようになっており、calimは初期配布のmintを使用するものとしてもう少し制約をつけて使うか。それともmintの機能をそもそも無くしてしまっても良いかもしれない。自分が最初に持つので。
function mint(address _to, uint256 _count) public payable saleIsOpen {
uint256 total = _totalSupply();
require(total + _count <= MAX_ELEMENTS, "Max limit");
require(total <= MAX_ELEMENTS, "Sale end");
require(_count <= MAX_BY_MINT, "Exceeds number");
require(msg.value >= price(_count), "Value below price");
for (uint256 i = 0; i < _count; i++) {
_mintAnElement(_to);
}
}
function _mintAnElement(address _to) private {
uint id = _totalSupply();
_tokenIdTracker.increment();
_safeMint(_to, id);
emit CreatePenguin(id);
}
また、withdrawを機能としてつけるべきか考える。制作者をownerに固定するためここは組み込むか考える。売却した際にコントラクトにたまるethをownerが回収できるようにする。これはmappingにてtokenidの所有者にownerが組み込まれるようにすればよい。一方で売却後に、売却先の相手が他の方に販売したら転売者に販売分のお金が入るようになっているかは確認しておく。
function withdrawAll() public payable onlyOwner {
uint256 balance = address(this).balance;
require(balance > 0);
_widthdraw(devAddress, balance.mul(35).div(100));
_widthdraw(creatorAddress, address(this).balance);
}
function _widthdraw(address _address, uint256 _amount) private {
(bool success, ) = _address.call{value: _amount}("");
require(success, "Transfer failed.");
}
結局販売のところはopenseaなどのプラットフォーム利用が便利なので、どのように作用しているかは理解しておく必要があるかもしれない。
Mint後の売買の流れはERC721に準拠することになるかと思うのでそちらを確認。
approveの流れからtransfer、transferfromを説明していく。
Openseaを使用する際には基本的にはtransferfromの使用での販売になるので、approveは必須となる。
Approveは、transfer先が対象トークンの保有者ではないこと、オペレーターに既に処理を委ねた isApprovedForAll(owner, _msgSender()) == true である際によく使われる。address owner はaddress operator をOperatorとすることを承認しているので売買の仲介プラットフォームとして信用していることになる。
ERC721には所有権の移動の仕組みはあるが販売の内容はなかったので、そちらは別途作る必要がある。
require(msg.value >= price(_count), "Value below price”);などが重要になる。
Price関係で見つかったのは下記ぐらい。
function price(uint256 _count) public pure returns (uint256) {
return PRICE.mul(_count);
}
Withdraw部分が価格の請求部分になるのか確認。
Openseaはどこの部分のコントラクトとコミュニケートを行なってオークションや指値価格設定などをしているのかを別途調べる必要がありそう。
基本的にコントラクトの中にpayableを書いたfunctionを設置するだけで、そのコントラクトへの入金が可能になる。withdrawが出金の方で使われることが多いよう。
contract Simplebank{
function deposit()public payable{}
function withdraw ()public{
//宛先payable(msg.sender)
//送金と額transfer(address(this).balance)
payable(msg.sender).transfer(address(this).balance);
}
}
これに記録機能であるmappingも組み込むと
contract Bank{
mapping(address=>uint)balance;
function deposit()public payable{
//本番ではsafemathなどを使うように
balance[msg.sender]+=msg.value;
}
function withdraw (uint _amount)public{
balance[msg.sender]-=_amount;
payable(msg.sender).transfer(_amount);
}
//Bankなので各自の口座を持たせるbalance内での資金移動にしてしまう
function transfer (address _to, uint _amount)public{
balance[msg.sender]-=_amount;
balance[_to]+=_amount;
}
}
これを踏まえてwithdrawの部分をもう一度見直す。