diff --git a/contracts/sources-registry.fc b/contracts/sources-registry.fc index 6f9395f..d53af4d 100644 --- a/contracts/sources-registry.fc +++ b/contracts/sources-registry.fc @@ -24,6 +24,7 @@ const int error::invalid_cell_code = 902; const int error::too_much_value = 901; const int error::not_enough_value = 900; const int error::access_denied = 401; +const int error::verifier_id_mismatch = 402; const int error::unknown_op = 0xffff; const int min_tons_lower_bound = 65000000; ;; 0.065 TON @@ -86,25 +87,35 @@ slice calculate_source_item_address(int wc, cell state_init) { return (); } slice sender_address = cs~load_msg_addr(); - - int op = in_msg_body~load_uint(32); - int query_id = in_msg_body~load_uint(64); - + var (min_tons, max_tons, admin, verifier_registry, source_item_code) = load_data(); - if (op == op::deploy_source_item) { - throw_unless(error::access_denied, equal_slices(sender_address, verifier_registry)); - throw_if(error::too_much_value, msg_value > max_tons); - throw_if(error::not_enough_value, msg_value < min_tons_lower_bound); - throw_if(error::not_enough_value, msg_value < min_tons); - int verifier_id = in_msg_body~load_uint(256); - int verified_code_cell_hash = in_msg_body~load_uint(256); - cell source_content = in_msg_body~load_ref(); - in_msg_body.end_parse(); - deploy_source_item(verifier_id, verified_code_cell_hash, source_item_code, source_content); - return (); + if (equal_slices(sender_address, verifier_registry)) { + ;; the verifier id authenticated by the verifier registry + slice verifier_reg_cell = in_msg_body~load_ref().begin_parse(); + int verified_verifier_id = verifier_reg_cell~load_uint(256); + slice forwarded_message = in_msg_body~load_ref().begin_parse(); + + int op = forwarded_message~load_uint(32); + int query_id = forwarded_message~load_uint(64); + + if (op == op::deploy_source_item) { + throw_if(error::too_much_value, msg_value > max_tons); + throw_if(error::not_enough_value, msg_value < min_tons_lower_bound); + throw_if(error::not_enough_value, msg_value < min_tons); + int verifier_id = forwarded_message~load_uint(256); + throw_unless(error::verifier_id_mismatch, verifier_id == verified_verifier_id); + int verified_code_cell_hash = forwarded_message~load_uint(256); + cell source_content = forwarded_message~load_ref(); + forwarded_message.end_parse(); + deploy_source_item(verifier_id, verified_code_cell_hash, source_item_code, source_content); + return (); + } } + int op = in_msg_body~load_uint(32); + int query_id = in_msg_body~load_uint(64); + if (op == op::change_verifier_registry) { throw_unless(error::access_denied, equal_slices(sender_address, admin)); slice new_verifier_registry = in_msg_body~load_msg_addr(); diff --git a/contracts/verifier-registry.fc b/contracts/verifier-registry.fc index 988fe59..a2de7cd 100644 --- a/contracts/verifier-registry.fc +++ b/contracts/verifier-registry.fc @@ -18,7 +18,7 @@ const slice REGISTERED_TEXT = "You were successfully registered as a verifier"; const slice UPDATED_TEXT = "You successfully updated verifier data"; const int EXIT_FEE = 200000000; ;; 0.20 TON -const int STAKE = 10000000000000; ;; 10000 TON +const int STAKE = 1000000000000; ;; 1000 TON () save_data() impure inline_ref { set_data(begin_cell() @@ -69,6 +69,7 @@ const int STAKE = 10000000000000; ;; 10000 TON () update_verifier(int id, slice new_settings, slice sender_address, int balance, int coins_sent) impure inline { slice admin = sender_address; throw_if(402, new_settings.slice_depth() > 10); ;; should allow for 100 nodes, prevents storage attack + (slice cfg, int ok) = storage::verifiers.udict_get?(256, id); if (ok) { ;; exists, check is admin admin = cfg~load_msg_addr(); @@ -146,7 +147,13 @@ const int STAKE = 10000000000000; ;; 10000 TON .store_slice(target_addr) .store_coins(0) .store_uint(1, 1 + 4 + 4 + 64 + 32 + 1 + 1) - .store_ref(payload_to_forward).end_cell(), 64); + .store_ref( + begin_cell() + .store_ref(begin_cell().store_uint(verifier_id, 256).end_cell()) ;; attach the verifier id so downstream contract can ensure message is from the correct source + .store_ref(payload_to_forward) + .end_cell() + ) + .end_cell(), 64); save_data(); } @@ -180,7 +187,9 @@ const int STAKE = 10000000000000; ;; 10000 TON slice new_settings = in_msg_body; ;; validation - in_msg_body~load_uint(8); ;; quorum + int quorum = in_msg_body~load_uint(8); + throw_unless(421, quorum > 0); + in_msg_body~load_dict(); ;; pub_key_endpoints slice name_ref = in_msg_body~load_ref().begin_parse(); ;; name throw_unless(420, string_hash(name_ref) == id); diff --git a/package-lock.json b/package-lock.json index 29854f3..be2fbe5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,7 +7,6 @@ "": { "name": "tonstarter-contracts", "version": "0.0.0", - "hasInstallScript": true, "license": "MIT", "dependencies": { "@aws-crypto/sha256-js": "^2.0.1", @@ -21,6 +20,7 @@ "@ton-community/sandbox": "^0.11.0", "@ton-community/test-utils": "^0.2.0", "@types/chai": "^4.3.0", + "@types/inquirer": "^9.0.7", "@types/mocha": "^9.0.0", "@types/semver": "^7.3.9", "axios-request-throttle": "^1.0.0", @@ -734,6 +734,16 @@ "integrity": "sha512-hC7OMnszpxhZPduX+m+nrx+uFoLkWOMiR4oa/AZF3MuSETYTZmFfJAHqZEM8MVlvfG7BEUcgvtwoCTxBp6hm3g==", "dev": true }, + "node_modules/@types/inquirer": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/@types/inquirer/-/inquirer-9.0.7.tgz", + "integrity": "sha512-Q0zyBupO6NxGRZut/JdmqYKOnN95Eg5V8Csg3PGKkP+FnvsUZx1jAyK7fztIszxxMuoBA6E3KXWvdZVXIpx60g==", + "dev": true, + "dependencies": { + "@types/through": "*", + "rxjs": "^7.2.0" + } + }, "node_modules/@types/long": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", @@ -758,6 +768,15 @@ "integrity": "sha512-L/TMpyURfBkf+o/526Zb6kd/tchUP3iBDEPjqjb+U2MAJhVRxxrmr2fwpe08E7QsV7YLcpq0tUaQ9O9x97ZIxQ==", "dev": true }, + "node_modules/@types/through": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/@types/through/-/through-0.0.33.tgz", + "integrity": "sha512-HsJ+z3QuETzP3cswwtzt2vEIiHBk/dCcHGhbmG5X3ecnwFD/lPrMpliGXxSCg03L9AhrdwA4Oz/qfspkDW+xGQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@ungap/promise-all-settled": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", @@ -4141,6 +4160,16 @@ "integrity": "sha512-hC7OMnszpxhZPduX+m+nrx+uFoLkWOMiR4oa/AZF3MuSETYTZmFfJAHqZEM8MVlvfG7BEUcgvtwoCTxBp6hm3g==", "dev": true }, + "@types/inquirer": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/@types/inquirer/-/inquirer-9.0.7.tgz", + "integrity": "sha512-Q0zyBupO6NxGRZut/JdmqYKOnN95Eg5V8Csg3PGKkP+FnvsUZx1jAyK7fztIszxxMuoBA6E3KXWvdZVXIpx60g==", + "dev": true, + "requires": { + "@types/through": "*", + "rxjs": "^7.2.0" + } + }, "@types/long": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", @@ -4165,6 +4194,15 @@ "integrity": "sha512-L/TMpyURfBkf+o/526Zb6kd/tchUP3iBDEPjqjb+U2MAJhVRxxrmr2fwpe08E7QsV7YLcpq0tUaQ9O9x97ZIxQ==", "dev": true }, + "@types/through": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/@types/through/-/through-0.0.33.tgz", + "integrity": "sha512-HsJ+z3QuETzP3cswwtzt2vEIiHBk/dCcHGhbmG5X3ecnwFD/lPrMpliGXxSCg03L9AhrdwA4Oz/qfspkDW+xGQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@ungap/promise-all-settled": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", diff --git a/package.json b/package.json index 87b6612..5cc6bd8 100644 --- a/package.json +++ b/package.json @@ -10,14 +10,16 @@ "test": "node --no-experimental-fetch node_modules/mocha/bin/mocha --exit test/**/*.spec.ts" }, "devDependencies": { + "@swc/core": "^1.2.177", "@ton-community/blueprint": "^0.10.0", "@ton-community/sandbox": "^0.11.0", "@ton-community/test-utils": "^0.2.0", - "@swc/core": "^1.2.177", "@types/chai": "^4.3.0", + "@types/inquirer": "^9.0.7", "@types/mocha": "^9.0.0", "@types/semver": "^7.3.9", "axios-request-throttle": "^1.0.0", + "bigint-buffer": "^1.1.5", "chai": "^4.3.4", "dotenv": "^16.0.0", "fast-glob": "^3.2.11", @@ -28,8 +30,7 @@ "ton-core": "^0.49.0", "ton-crypto": "^3.2.0", "ts-node": "^10.4.0", - "typescript": "^4.5.4", - "bigint-buffer": "^1.1.5" + "typescript": "^4.5.4" }, "prettier": { "printWidth": 100 diff --git a/scripts/set-code-sources-registry.ts b/scripts/set-code-sources-registry.ts new file mode 100644 index 0000000..493c293 --- /dev/null +++ b/scripts/set-code-sources-registry.ts @@ -0,0 +1,38 @@ +import { toNano, Address } from "ton-core"; +import { SourcesRegistry } from "../wrappers/sources-registry"; +import { compile, NetworkProvider } from "@ton-community/blueprint"; +import inquirer from "inquirer"; + +export async function run(provider: NetworkProvider) { + const compiled = await compile("sources-registry"); + + const codeCellHash = compiled.hash().toString("base64"); + const addressToConfirm = Address.parse("[SOURCES_REGISTRY_ADDRESS]"); + + const sourcesRegistry = provider.open(SourcesRegistry.createFromAddress(addressToConfirm)); + + const { address } = await inquirer.prompt([ + { + type: "input", + name: "address", + message: `\n\n!!!This will set the code of the Sources Registry contract!!! Proceed with extreme caution!\n\nSources Registry Address: ${addressToConfirm}\nNew Code Cell Hash: ${codeCellHash}\n\nWrite the address of the contract to confirm:`, + }, + ]); + + try { + if (!Address.parse(address).equals(addressToConfirm)) { + console.log("Address does not match, aborting..."); + process.exit(1); + } + } catch (e) { + console.log(`Invalid address: ${address}, aborting...`); + process.exit(1); + } + + await sourcesRegistry.sendChangeCode(provider.sender(), { + newCode: compiled, + value: toNano("0.05"), + }); + + console.log("Source Registry set code", sourcesRegistry.address); +} diff --git a/test/unit/sources-registry.spec.ts b/test/unit/sources-registry.spec.ts index 8610342..b553ec1 100644 --- a/test/unit/sources-registry.spec.ts +++ b/test/unit/sources-registry.spec.ts @@ -41,10 +41,10 @@ describe("Sources", () => { sourceRegistryContract = blockchain.openContract( SourcesRegistry.create( { - admin: admin.address, + admin: admin.address, verifierRegistryAddress, - sourceItemCode - }, + sourceItemCode, + }, code ) ); @@ -63,7 +63,6 @@ describe("Sources", () => { it("should deploy a source contract item", async () => { const send = await sourceRegistryContract.sendDeploySource( blockchain.sender(verifierRegistryAddress), - { verifierId: specs[0].verifier, codeCellHash: specs[0].codeCellHash, @@ -88,18 +87,23 @@ describe("Sources", () => { expect(await parseUrlFromGetSourceItemData(sourceItemContract)).to.equal(specs[0].jsonURL); }); - it("disallows a non-verifier reg to deploy a source item", async () => { - const notVerifier = await blockchain.treasury("non-verifier"); - const send = await sourceRegistryContract.sendDeploySource(notVerifier.getSender(), { - verifierId: specs[0].verifier, - codeCellHash: specs[0].codeCellHash, - jsonURL: specs[0].jsonURL, - version: 1, - value: toNano("0.5"), - }); + it("disallows a spoofed verifier id to set a deploy item", async () => { + const send = await sourceRegistryContract.sendDeploySource( + blockchain.sender(verifierRegistryAddress), + + { + verifierId: specs[0].verifier, + codeCellHash: specs[0].codeCellHash, + jsonURL: specs[0].jsonURL, + version: 1, + value: toNano("0.5"), + }, + "spoofedVerifier" + ); + expect(send.transactions).to.have.transaction({ - from: notVerifier.address, - exitCode: 401, + from: verifierRegistryAddress, + exitCode: 402, }); }); }); @@ -459,6 +463,19 @@ describe("Sources", () => { }); }); }); + + describe("No op", () => { + it("throws on no ops", async () => { + const notVerifier = await blockchain.treasury("non-verifier"); + + const send = await sourceRegistryContract.sendNoOp(notVerifier.getSender()); + + expect(send.transactions).to.have.transaction({ + from: notVerifier.address, + exitCode: 0xffff, + }); + }); + }); }); async function parseUrlFromGetSourceItemData( diff --git a/test/unit/verifier-registry.spec.ts b/test/unit/verifier-registry.spec.ts index 9c7a7ff..f0cc407 100644 --- a/test/unit/verifier-registry.spec.ts +++ b/test/unit/verifier-registry.spec.ts @@ -204,7 +204,7 @@ describe("Verifier Registry", () => { if (exit.info.type === "internal") { expect(Number(exit.info.value.coins)).to.be.gte( // TODO - Number(toNano(10000) - toNano("0.2")) + Number(toNano(1000) - toNano("0.2")) ); } // expect(exit.mode).to.equal(64); TODO @@ -214,12 +214,11 @@ describe("Verifier Registry", () => { expect(body.loadBuffer(body.remainingBits / 8).toString()).to.equal( "Withdrawal and exit from the verifier registry" ); - + // fails due to https://github.com/ton-core/ton-core/pull/28 // let data = await verifierRegistry.getVerifier(sha256BN("verifier1")); // expect(data.settings).to.equal(null); - let verifiers = await verifierRegistry.getVerifiers(); expect(verifiers.length).to.equal(0); @@ -276,13 +275,15 @@ describe("Verifier Registry", () => { aborted: false, }); - let outMessages = transactionsFrom(res.transactions, src)[0].outMessages; - let excess = outMessages.values()[0]; + const outMessages = transactionsFrom(res.transactions, src)[0].outMessages; + const excess = outMessages.values()[0]; expect(excess.info.dest).to.equalAddress(dst); // expect(excess.mode).to.equal(64); - let body = excess.body.beginParse(); - expect(body.loadUint(32)).to.equal(777); + const body = excess.body.beginParse(); + const [verifierIdCell, msgCell] = [body.loadRef().beginParse(), body.loadRef().beginParse()]; + expect(verifierIdCell.loadUintBig(256)).to.equal(sha256BN("verifier1")); + expect(msgCell.loadUint(32)).to.equal(777); }); it("should forward message, 2 out of 3 correct, quorum = 2", async () => { @@ -314,7 +315,9 @@ describe("Verifier Registry", () => { // expect(excess.mode).to.equal(64); TODO let body = excess.body.beginParse(); - expect(body.loadUint(32)).to.equal(777); + const [verifierIdCell, msgCell] = [body.loadRef().beginParse(), body.loadRef().beginParse()]; + expect(verifierIdCell.loadUintBig(256)).to.equal(sha256BN("verifier1")); + expect(msgCell.loadUint(32)).to.equal(777); }); it("should not forward message, 1 sign of 2", async () => { @@ -483,7 +486,7 @@ describe("Verifier Registry", () => { endpoints: new Map([[toBigIntBE(kp3.publicKey), ip2num("10.0.0.1")]]), name: "verifier2", marketingUrl: "https://myverifier.com", - value: toNano(10005), + value: toNano(1005), }); expect(res.transactions).to.have.transaction({ @@ -519,6 +522,27 @@ describe("Verifier Registry", () => { ); }); + it("shouldn't allow setting a 0 quorum", async () => { + let user = randomAddress("user"); + + let kp3 = await randomKeyPair(); + + let res = await verifierRegistry.sendUpdateVerifier(blockchain.sender(user), { + id: sha256BN("verifier2"), + quorum: 0, + endpoints: new Map([[toBigIntBE(kp3.publicKey), ip2num("10.0.0.1")]]), + name: "verifier2", + marketingUrl: "https://myverifier.com", + value: toNano(10005), + }); + + expect(res.transactions).to.have.transaction({ + from: user, + exitCode: 421, + aborted: true, + }); + }); + it("should not add new verifier, 20 limit", async () => { let cfg = await genDefaultVerifierRegistryConfig(admin); verifierRegistry = blockchain.openContract( diff --git a/wrappers/sources-registry.ts b/wrappers/sources-registry.ts index f4ec8c7..4053d11 100644 --- a/wrappers/sources-registry.ts +++ b/wrappers/sources-registry.ts @@ -13,6 +13,7 @@ import { import { toBigIntBE } from "bigint-buffer"; import { Sha256 } from "@aws-crypto/sha256-js"; +import { sha256BN } from "../test/unit/helpers"; export function sourceRegistryConfigToCell(params: { minTons: bigint; @@ -45,11 +46,11 @@ export class SourcesRegistry implements Contract { static create( params: { - verifierRegistryAddress: Address, - admin: Address, - sourceItemCode: Cell, - minTons?: bigint, - maxTons?: bigint, + verifierRegistryAddress: Address; + admin: Address; + sourceItemCode: Cell; + minTons?: bigint; + maxTons?: bigint; }, code: Cell, workchain = 0 @@ -135,7 +136,8 @@ export class SourcesRegistry implements Contract { jsonURL: string; version: number; value: bigint; - } + }, + verifiedVerifierId = params.verifierId ) { const body = beginCell() .storeUint(1002, 32) @@ -147,7 +149,10 @@ export class SourcesRegistry implements Contract { await provider.internal(via, { value: params.value, sendMode: SendMode.PAY_GAS_SEPARATELY, - body, + body: beginCell() + .storeRef(beginCell().storeUint(sha256BN(verifiedVerifierId), 256).endCell()) + .storeRef(body) + .endCell(), }); } @@ -236,4 +241,12 @@ export class SourcesRegistry implements Contract { body, }); } + + async sendNoOp(provider: ContractProvider, via: Sender) { + await provider.internal(via, { + value: toNano("0.5"), + sendMode: SendMode.PAY_GAS_SEPARATELY, + body: beginCell().storeUint(8888, 32).storeUint(0, 64).endCell(), + }); + } }