From 87067210b45211cfa3710004c6b5a4a09c2c309f Mon Sep 17 00:00:00 2001 From: ennucore Date: Sat, 7 Jan 2023 19:40:12 +0100 Subject: [PATCH] Wrote signing and tests for it --- contracts/imports/dns-utils.fc | 59 ++++++++++++++-------------- contracts/imports/op-codes.fc | 1 + contracts/main.ts | 56 ++++++++++++++++++++++----- contracts/nft-collection.fc | 45 ++++++++++++++++++--- contracts/nft-item.fc | 34 +--------------- test/creation.spec.ts | 2 +- test/selling.spec.ts | 56 --------------------------- test/signing.spec.ts | 71 ++++++++++++++++++++++++++++++++++ 8 files changed, 191 insertions(+), 133 deletions(-) delete mode 100644 test/selling.spec.ts create mode 100644 test/signing.spec.ts diff --git a/contracts/imports/dns-utils.fc b/contracts/imports/dns-utils.fc index abf1f18..efc0227 100644 --- a/contracts/imports/dns-utils.fc +++ b/contracts/imports/dns-utils.fc @@ -87,7 +87,7 @@ int check_domain_string(slice domain) { result1~store_uint(char, 8); } if (~ need_break & (i >= len)) { - throw(259); ;; no ';' found + throw(259); ;; no ';' found } } } until (need_break); @@ -102,48 +102,49 @@ int check_domain_string(slice domain) { int len = slice_bits(str); int need_break = 0; builder result = begin_cell(); - do { + int res_len = 0; + while ~ need_break { need_break = len == 0; if (~ need_break) { int char = str~load_uint(8); int byte = 0; - if (char >= 65) { - if (char <= 90) { ;; a-z -> 0-25 - byte = char - 65; + if ((char >= 65) & (char <= 90)) { + ;; Code for A-Z -> 0-25 + byte = char - 65; + } else { + if ((char >= 97) & (char <= 122)) { + ;; Code for a-z -> 26-51 + byte = char - 71; } else { - if (char >= 97) { ;; A-Z -> 26-51 - if (char <= 122) { - byte = char - 71; + if ((char >= 48) & (char <= 57)) { + ;; Code for 0-9 -> 52-61 + byte = char + 4; + } else { + if (char == 45) { + ;; Code for - -> 62 + byte = 62; } else { - if (char >= 48) { ;; 0-9 -> 52-61 - if (char <= 57) { - byte = char + 4; - } else { - if (char == 45) { ;; - -> 62 - byte = 62; - } else { - if (char == 95) { ;; _ -> 63 - byte = 63; - } else { - throw(260); ;; invalid character - } - } - } + if (char == 95) { + ;; Code for _ -> 63 + byte = 63; } else { - throw(260); ;; invalid character + throw(260); ;; invalid character } } - } else { - throw(260); ;; invalid character } } - } else { - throw(260); ;; invalid character } - result~store_uint(byte, 6); len -= 8; + + if (len > 0) { + result~store_uint(byte, 6); + } else { + ;; Last byte + result~store_uint(byte, 8 - mod(res_len, 8)); + } + res_len += 6; } - } until (need_break); + } return result.end_cell().begin_parse(); } diff --git a/contracts/imports/op-codes.fc b/contracts/imports/op-codes.fc index 1a74e64..c01879a 100644 --- a/contracts/imports/op-codes.fc +++ b/contracts/imports/op-codes.fc @@ -19,6 +19,7 @@ int op::editorship_assigned() asm "0x511a4463 PUSHINT"; int op::new_nft() asm "0x1a039a51 PUSHINT"; int op::instant_buy_new_nft() asm "0x16c7d435 PUSHINT"; int op::init_after_buy() asm "0x437dc408 PUSHINT"; +int op::collect_money() asm "0x2a0c8a20 PUSHINT"; ;; DNS const int op::fill_up = 0x370fec51; diff --git a/contracts/main.ts b/contracts/main.ts index 0a8fb0c..075038e 100644 --- a/contracts/main.ts +++ b/contracts/main.ts @@ -1,10 +1,11 @@ import BN from "bn.js"; -import {Cell, beginCell, Address} from "ton"; +import {Cell, beginCell, Address, Slice} from "ton"; import {C7Config, SmartContract} from "ton-contract-executor"; import {encodeOffChainContent, makeSnakeCell} from "./utils"; import {randomBytes} from "crypto"; -import {keyPairFromSeed, KeyPair, sign} from "ton-crypto"; -import {ExpansionPanelActions} from "@material-ui/core"; +import {keyPairFromSeed, KeyPair, sign, keyPairFromSecretKey} from "ton-crypto"; +import {randomAddress} from "../test/helpers"; +import {hashCell} from "ton/dist/boc/boc"; // encode contract storage according to save_data() contract method @@ -37,7 +38,7 @@ export function data(params: { ownerAddress: Address; collectionAddress: Address } export function collectionData(params: { - code: Cell, ownerAddress: Address, ownerKey: number, + code: Cell, ownerAddress: Address, ownerKey: BN, price_multiplier?: number, price_steepness?: number }): Cell { if (params.price_multiplier == undefined) { @@ -59,8 +60,43 @@ export function auctionWithWinner(winnerAddress: Address) { return beginCell().storeAddress(winnerAddress).storeCoins(0).storeUint(0, 64) } -export function setContractBalance(contract: SmartContract, balance: number) { - contract.setC7Config({balance: balance}); +export function setContractBalance(contract: SmartContract, balance: number, address?: Address) { + if (address == undefined) { + address = randomAddress("collection"); + } + contract.setC7Config({balance: balance, myself: address}); +} + +export function asciiEncode(buf: Buffer): string { + let sl = beginCell().storeBuffer(buf).endCell().beginParse(); + let result = ""; + while (sl.remaining) { + let byte; + if (sl.remaining >= 6) { + byte = sl.readUint(6).toNumber(); + } else { + byte = sl.readUint(sl.remaining).toNumber(); + } + if (byte < 26) { + result += String.fromCharCode(byte + 65); // 0-25 -> A-Z + } else if (byte < 52) { + result += String.fromCharCode(byte + 97 - 26); // 26-51 -> a-z + } else if (byte < 62) { + result += String.fromCharCode(byte + 48 - 52); // 52-61 -> 0-9 + } else if (byte == 62) { + result += '-'; // 62 -> - + } else if (byte == 63) { + result += '_'; // 63 -> _ + } + } + return result; +} + +export function signBuy(domain: string, collectionAddress: Address, buyerAddress: Address, ownerKey: Buffer): string { + let signData = beginCell().storeAddress(collectionAddress).storeBuffer(Buffer.from(domain)) + .storeAddress(buyerAddress).storeUint(0, 2).endCell(); + let signature = sign(hashCell(signData), ownerKey); + return asciiEncode(signature); } export function TON(): number { @@ -73,11 +109,13 @@ export function transferOwnership(params: { newOwnerAddress: Address }): Cell { return beginCell().storeUint(0x5fcc3d14, 32).storeUint(0, 64).storeAddress(params.newOwnerAddress).storeAddress(null).storeInt(0, 1).storeCoins(1 * TON()).endCell(); } -export function createItem(params: { domain: String }): Cell { - let signature = '000'; +export function createItem(params: { domain: String, signature?: String }): Cell { + if (params.signature == undefined) { + params.signature = "000"; + } return beginCell() .storeUint(0, 32) - .storeRef(makeSnakeCell(Buffer.from(params.domain + ';' + signature))) + .storeRef(makeSnakeCell(Buffer.from(params.domain + ';' + params.signature))) .endCell(); } diff --git a/contracts/nft-collection.fc b/contracts/nft-collection.fc index 359a29b..9ae5936 100644 --- a/contracts/nft-collection.fc +++ b/contracts/nft-collection.fc @@ -3,8 +3,6 @@ #include "imports/op-codes.fc"; #include "imports/params.fc"; -;; -1 if needed, 0 if not -const signature_needed = 0; ;; storage scheme ;; cell collection_content @@ -18,7 +16,7 @@ const signature_needed = 0; return ( ds~load_ref(), ;; content ds~load_ref(), ;; nft_item_code - ds~load_ref(), ;; pricing + ds~load_ref(), ;; pricing ds~load_uint(256), ;; owner key ds~load_msg_addr() ;; owner address ); @@ -61,6 +59,22 @@ slice calculate_nft_item_address(int wc, cell state_init) { .begin_parse(); } +() send_msg(slice to_address, int amount, int op, int query_id, builder payload, int send_mode) impure inline { + var msg = begin_cell() + .store_uint(0x10, 6) ;; nobounce - int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool src:MsgAddress -> 010000 + .store_slice(to_address) + .store_coins(amount) + .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1) + .store_uint(op, 32) + .store_uint(query_id, 64); + + if (~ builder_null?(payload)) { + msg = msg.store_builder(payload); + } + + send_raw_message(msg.end_cell(), send_mode); +} + () deploy_nft_item(int item_index, cell nft_item_code, cell nft_content) impure { cell state_init = calculate_nft_item_state_init(item_index, nft_item_code); slice nft_address = calculate_nft_item_address(workchain(), state_init); @@ -75,7 +89,8 @@ slice calculate_nft_item_address(int wc, cell state_init) { } int verify_signature(slice signature, slice sender_address, slice domain, int owner_key) { - cell option_data = begin_cell().store_slice(my_address()).store_slice(domain).store_slice(sender_address).end_cell(); + cell option_data = begin_cell().store_slice(my_address()) + .store_slice(domain).store_slice(sender_address).store_uint(0, 2).end_cell(); return check_signature(slice_hash(option_data.begin_parse()), signature, owner_key); } @@ -105,8 +120,10 @@ int verify_signature(slice signature, slice sender_address, slice domain, int ow int item_index = slice_hash(domain); slice sender_address = cs~load_msg_addr(); - if (signature_needed) { + if (key != 0) { slice signature = decode_asciicode(signature_encoded); + int bbb = signature.preload_uint(8); +;; throw(300 + slice_bits(signature)); throw_unless(205, verify_signature(signature, sender_address, domain, key)); } @@ -121,6 +138,13 @@ int verify_signature(slice signature, slice sender_address, slice domain, int ow if (op == op::fill_up) { ;; just fill-up balance return (); } + + if (op == op::collect_money()) { + slice sender_address = cs~load_msg_addr(); + throw_unless(403, equal_slices(sender_address, addr)); + send_msg(addr, msg_value, op::collect_money(), 0, null(), 1); + return (); + } throw(0xffff); } @@ -146,13 +170,22 @@ int get_price(slice domain) method_id { return calcprice(domain, pricing); } +int signature_data(slice sender_address, slice domain) method_id { + cell option_data = begin_cell().store_slice(my_address()).store_slice(domain).store_slice(sender_address).end_cell(); + return slice_hash(option_data.begin_parse()); +} + +slice getaddr() method_id { + return my_address(); +} + (int, cell) dnsresolve(slice subdomain, int category) method_id { throw_unless(70, mod(slice_bits(subdomain), 8) == 0); int starts_with_zero_byte = subdomain.preload_int(8) == 0; if (starts_with_zero_byte & (slice_bits(subdomain) == 8)) { ;; "." requested - return (8, null()); ;; resolved but no dns-records + return (8, null()); ;; resolved but no dns-records } if (starts_with_zero_byte) { subdomain~load_uint(8); diff --git a/contracts/nft-item.fc b/contracts/nft-item.fc index cdd40ae..d347136 100644 --- a/contracts/nft-item.fc +++ b/contracts/nft-item.fc @@ -119,43 +119,13 @@ int min_tons_for_storage() asm "1000000000 PUSHINT"; ;; 1 TON int op = in_msg_body.slice_empty?() ? 0 : in_msg_body~load_uint(32); - - if (op == 0) { ;; todo -;; if (auction_complete) { -;; throw_unless(406, equal_slices(sender_address, owner_address)); ;; only owner can fill-up balance, prevent coins lost right after the auction -;; ;; if owner send bid right after auction he can restore it by transfer resonse message - store_data(index, collection_address, owner_address, content, domain, now()); -;; } else { -;; throw_unless(407, msg_value >= muldiv(max_bid_amount, 105, 100)); ;; 5% greater then previous bid -;; int amount_to_send = (max_bid_amount > my_balance - min_tons_for_storage()) ? (my_balance - min_tons_for_storage()) : max_bid_amount; -;; if (amount_to_send > 0) { -;; send_msg(max_bid_address, amount_to_send, op::outbid_notification, cur_lt(), null(), 1); ;; pay transfer fees separately -;; } -;; max_bid_amount = msg_value; -;; max_bid_address = sender_address; -;; int delta_time = auction_prolongation - (auction_end_time - now()); -;; if (delta_time > 0) { -;; auction_end_time += delta_time; -;; } -;; store_data(index, collection_address, owner_address, content, domain, now()); -;; } - + if (op == 0) { + store_data(index, collection_address, owner_address, content, domain, now()); return (); } int query_id = in_msg_body~load_uint(64); -;; if ((auction_complete) & (~ cell_null?(auction))) { ;; take domain after auction ;; todo -;; int balance_without_msg = my_balance - msg_value; -;; int amount_to_send = (max_bid_amount > balance_without_msg - min_tons_for_storage()) ? (balance_without_msg - min_tons_for_storage()) : max_bid_amount; -;; if (amount_to_send > 0) { -;; send_msg(collection_address, amount_to_send, op::fill_up, query_id, null(), 2); ;; ignore errors -;; my_balance -= amount_to_send; -;; } -;; owner_address = max_bid_address; -;; auction = null(); -;; store_data(index, collection_address, owner_address, content, domain, last_fill_up_time); -;; } if (op == op::transfer()) { throw_unless(401, equal_slices(sender_address, owner_address)); diff --git a/test/creation.spec.ts b/test/creation.spec.ts index 277c50c..f38dee2 100644 --- a/test/creation.spec.ts +++ b/test/creation.spec.ts @@ -36,7 +36,7 @@ describe("Creating items tests", () => { value: new BN(100 * main.TON()), }); const res = await contract.sendInternalMessage(sendToSelfMessage); - console.log(res); + expect(res.type).to.equal("success"); expect(res.exit_code).to.equal(0); }); diff --git a/test/selling.spec.ts b/test/selling.spec.ts deleted file mode 100644 index d6eb81d..0000000 --- a/test/selling.spec.ts +++ /dev/null @@ -1,56 +0,0 @@ -import chai, { assert, expect } from "chai"; -import chaiBN from "chai-bn"; -import BN from "bn.js"; -chai.use(chaiBN(BN)); - -import { Builder, Cell, Slice } from "ton"; -import { SmartContract } from "ton-contract-executor"; -import * as main from "../contracts/main"; -import { internalMessage, randomAddress } from "./helpers"; - -import { hex } from "../build/main.compiled.json"; -import { makeSnakeCell } from "../contracts/utils"; -import { signVerify } from "ton-crypto"; - -describe("Selling tests (instant buy)", () => { - let contract: SmartContract; - let debug: boolean = true; - let keyPair = main.genKeyPair(); - - beforeEach(async () => { - contract = await SmartContract.fromCell( - Cell.fromBoc(hex)[0], - main.data({ - ownerAddress: randomAddress("owner"), - code: Cell.fromBoc(hex)[0], - collectionAddress: randomAddress("collection"), - domain: "alice", - publicKey: keyPair.publicKey - }), - { debug: debug } - ); - }); - - it("Sell", async () => { - main.setContractBalance(contract, 10 * main.TON()); - const sellMessage = internalMessage({ - from: randomAddress("buyer"), - body: main.instantBuyMessage({ - receiverAddress: randomAddress("buyer"), - issuedCollectionAddr: randomAddress("collection"), - price: 10 * main.TON(), - domain: "bob", - privateKey: keyPair.secretKey - }), - value: new BN(10 * main.TON()) - }); - context("Public key is correct", async () => { - const pubKeyRes = await contract.invokeGetMethod("get_public_key", []); - expect(pubKeyRes.result[0]).to.equal(keyPair.publicKey); - }); - - const res = await contract.sendInternalMessage(sellMessage); - expect(res.type).to.equal("success"); - expect(res.exit_code).to.equal(0); - }); -}); diff --git a/test/signing.spec.ts b/test/signing.spec.ts new file mode 100644 index 0000000..e94490c --- /dev/null +++ b/test/signing.spec.ts @@ -0,0 +1,71 @@ +import chai, {assert, expect} from "chai"; +import chaiBN from "chai-bn"; +import BN from "bn.js"; + +chai.use(chaiBN(BN)); + +import {Address, Builder, Cell, contractAddress, Slice} from "ton"; +import {runContract, SmartContract} from "ton-contract-executor"; +import * as main from "../contracts/main"; +import {internalMessage, randomAddress} from "./helpers"; + +import {hex} from "../build/nft-collection.compiled.json"; +import {makeSnakeCell} from "../contracts/utils"; +import {keyPairFromSeed, KeyPair, sign, keyPairFromSecretKey} from "ton-crypto"; +import {signBuy} from "../contracts/main"; + +let ownerKeys = keyPairFromSeed(Buffer.from("0000000000000000000000000000000000000000000000000000000000000000", "hex")); +let ownerPubNum = new BN(ownerKeys.publicKey); +let data = main.collectionData({ + ownerAddress: randomAddress("owner"), + code: Cell.fromBoc(hex)[0], + ownerKey: ownerPubNum, +}); + +describe("Creating items tests", () => { + let contract: SmartContract; + let debug: boolean = true; + + beforeEach(async () => { + contract = await SmartContract.fromCell( + Cell.fromBoc(hex)[0], + data, + {debug: debug} + ); + contract.setC7Config({ + myself: randomAddress("collection") + }) + }); + + it("allows to buy an item with a valid signature", async () => { + main.setContractBalance(contract, 10 * main.TON(), randomAddress("collection")); + let ownerAddr = randomAddress("dude"); + let signature = signBuy("test", randomAddress("collection"), ownerAddr, ownerKeys.secretKey); + const sendToSelfMessage = internalMessage({ + from: ownerAddr, + body: main.createItem({domain: "test", signature: signature}), + value: new BN(100 * main.TON()), + }); + const res = await contract.sendInternalMessage(sendToSelfMessage); + // console.log(res); + let fs = require('fs'); + fs.writeFile('logs.txt', res.logs, (_: any) => {}); + expect(res.type).to.equal("success"); + expect(res.exit_code).to.equal(0); + }); + it("does not allow to buy an item if the signature is invalid", async () => { + main.setContractBalance(contract, 10 * main.TON(), randomAddress("collection")); + let ownerAddr = randomAddress("dude"); + let signature = signBuy("test", randomAddress("collection"), ownerAddr, ownerKeys.secretKey); + const sendToSelfMessage = internalMessage({ + from: ownerAddr, + body: main.createItem({domain: "test", signature: signature}), + value: new BN(100 * main.TON()), + }); + const res = await contract.sendInternalMessage(sendToSelfMessage); + expect(res.type).to.equal("success"); + expect(res.exit_code).to.equal(0); + + }); + +});