diff --git a/include/atomicassets-interface.hpp b/include/atomicassets-interface.hpp index 396ac5b..b592385 100644 --- a/include/atomicassets-interface.hpp +++ b/include/atomicassets-interface.hpp @@ -148,19 +148,6 @@ namespace atomicassets { typedef multi_index assets_t; - struct holders_s { - uint64_t asset_id; - name holder; - name owner; - - uint64_t primary_key() const { return asset_id; }; - uint64_t by_holder() const { return holder.value; }; - }; - typedef multi_index >> - holders_t; - - struct offers_s { uint64_t offer_id; name sender; @@ -228,6 +215,5 @@ namespace atomicassets { template_mutables_t get_template_mutables(name collection_name) {return template_mutables_t(ATOMICASSETS_ACCOUNT, collection_name.value);} assets_t get_assets(name owner) {return assets_t(ATOMICASSETS_ACCOUNT, owner.value);} - holders_t get_holders() {return holders_t(ATOMICASSETS_ACCOUNT, ATOMICASSETS_ACCOUNT.value);} }; \ No newline at end of file diff --git a/include/atomicassets.hpp b/include/atomicassets.hpp index c8bf373..f6744f4 100644 --- a/include/atomicassets.hpp +++ b/include/atomicassets.hpp @@ -34,14 +34,6 @@ CONTRACT atomicassets : public contract { string memo ); - ACTION move( - name owner, - name from, - name to, - vector asset_ids, - string memo - ); - ACTION createcol( name author, name collection_name, @@ -260,15 +252,6 @@ CONTRACT atomicassets : public contract { string memo ); - ACTION logmove( - name collection_name, - name owner, - name from, - name to, - vector asset_ids, - string memo - ); - ACTION lognewoffer( uint64_t offer_id, name sender, @@ -438,19 +421,6 @@ CONTRACT atomicassets : public contract { typedef multi_index assets_t; - TABLE holders_s { - uint64_t asset_id; - name holder; - name owner; - - uint64_t primary_key() const { return asset_id; }; - uint64_t by_holder() const { return holder.value; }; - }; - typedef multi_index >> - holders_t; - - TABLE offers_s { uint64_t offer_id; name sender; @@ -518,7 +488,6 @@ CONTRACT atomicassets : public contract { template_mutables_t get_template_mutables(name collection_name) {return template_mutables_t(get_self(), collection_name.value);} assets_t get_assets(name owner) {return assets_t(get_self(), owner.value);} - holders_t get_holders() {return holders_t(get_self(), get_self().value);} /* ************************** diff --git a/src/atomicassets.cpp b/src/atomicassets.cpp index c46f7f9..a574b87 100644 --- a/src/atomicassets.cpp +++ b/src/atomicassets.cpp @@ -85,109 +85,6 @@ ACTION atomicassets::transfer( internal_transfer(from, to, asset_ids, memo, from); } -/** -* Moves one or more assets to another account -* @required_auth of the true owner of the asset -* Cannot have notifications for the from & to, exploitable -*/ -ACTION atomicassets::move( - name owner, - name from, - name to, - vector asset_ids, - string memo -) { - require_auth(owner); - require_recipient(owner); - - check(is_account(from), "from account does not exist"); - check(is_account(to), "to account does not exist"); - - check(from != to, "from & to fields cannot be the same"); - - check(asset_ids.size() != 0, "asset_ids needs to contain at least one id"); - check(memo.length() <= 256, "A move memo can only be 256 characters max"); - - vector asset_ids_copy = asset_ids; - std::sort(asset_ids_copy.begin(), asset_ids_copy.end()); - check(std::adjacent_find(asset_ids_copy.begin(), asset_ids_copy.end()) == asset_ids_copy.end(), - "Can't move the same asset multiple times"); - - assets_t owner_assets = get_assets(owner); - holders_t holders = get_holders(); - - map > collection_to_assets_moved = {}; - - for (uint64_t & asset_id : asset_ids) { - auto asset_itr = owner_assets.find(asset_id); - if (asset_itr == owner_assets.end()){ - check(false, - ("Owner doesn't own at least one of the provided assets (ID: " + to_string(asset_id) + ")").c_str()); - } - - - //Existence doesn't have to be checked because this always has to exist - if (asset_itr->template_id >= 0) { - templates_t collection_templates = get_templates(asset_itr->collection_name); - - auto template_itr = collection_templates.find(asset_itr->template_id); - if (!template_itr->transferable){ - check(false, - ("At least one asset isn't transferable (ID: " + to_string(asset_id) + ")").c_str()); - } - } - - auto holders_itr = holders.find(asset_id); - if (holders_itr == holders.end()){ - if (from != owner){ - check(false, - ("Only the owner can move this asset (ID: " + to_string(asset_id) + ")").c_str()); - } - - // Emplaces new holder - holders.emplace(owner, [&](auto &_holders_row){ - _holders_row.asset_id = asset_id; - _holders_row.holder = to; - _holders_row.owner = owner; - }); - } - - if (holders_itr != holders.end()){ - if (holders_itr->holder != from){ - check(false, - ("At least one asset invalidates the 'from:holder' constraint (ID: " + to_string(asset_id) + ")").c_str()); - } - - // Deletes row if returning to owner - if (to == owner){ - holders.erase(holders_itr); - } else { // Modifies row to move holdership to the new "to" wallet - holders.modify(holders_itr, owner, [&](auto &_holders_row){ - _holders_row.holder = to; - }); - } - } - - //This is needed for sending notifications later - if (collection_to_assets_moved.find(asset_itr->collection_name) != - collection_to_assets_moved.end()) { - collection_to_assets_moved[asset_itr->collection_name].push_back(asset_id); - } else { - collection_to_assets_moved[asset_itr->collection_name] = {asset_id}; - } - } - - // Sending notifications - for (const auto&[collection, assets_moved] : collection_to_assets_moved) { - action( - permission_level{get_self(), name("active")}, - get_self(), - name("logmove"), - make_tuple(collection, owner, from, to, assets_moved, memo) - ).send(); - } -} - /** * Creates a new collection */ @@ -1213,14 +1110,6 @@ ACTION atomicassets::burnasset( check(template_itr->burnable, "The asset is not burnable"); }; - holders_t holders = get_holders(); - - // Checks to see if the asset has been rented out & erases the "holdership" - auto holders_itr = holders.find(asset_id); - if (holders_itr != holders.end()){ - holders.erase(holders_itr); - } - if (asset_itr->backed_tokens.size() != 0) { auto balances = get_balances(); auto balance_itr = balances.find(asset_owner.value); @@ -1565,20 +1454,6 @@ ACTION atomicassets::logtransfer( notify_collection_accounts(collection_name); } -ACTION atomicassets::logmove( - name collection_name, - name owner, - name from, - name to, - vector asset_ids, - string memo -) { - require_auth(get_self()); - - notify_collection_accounts(collection_name); -} - - ACTION atomicassets::lognewoffer( uint64_t offer_id, name sender, @@ -1809,7 +1684,6 @@ void atomicassets::internal_transfer( assets_t from_assets = get_assets(from); assets_t to_assets = get_assets(to); - holders_t holders = get_holders(); map > collection_to_assets_transferred = {}; @@ -1831,19 +1705,6 @@ void atomicassets::internal_transfer( } } - auto holders_itr = holders.find(asset_id); - if (holders_itr != holders.end()){ - - // Deletes row if transfering to holder - if (to == holders_itr->holder){ - holders.erase(holders_itr); - } else { // Modifies row to move ownership to the new "to" wallet - holders.modify(holders_itr, from, [&](auto &_holders_row){ - _holders_row.owner = to; - }); - } - } - //This is needed for sending notifications later if (collection_to_assets_transferred.find(asset_itr->collection_name) != collection_to_assets_transferred.end()) { diff --git a/tests/asset-actions/move.test.js b/tests/asset-actions/move.test.js deleted file mode 100644 index 265fa60..0000000 --- a/tests/asset-actions/move.test.js +++ /dev/null @@ -1,504 +0,0 @@ -const { Blockchain, nameToBigInt, mintTokens, bigIntToName } = require("@vaulta/vert"); -const { Name } = require('@wharfkit/antelope'); -const fs = require('fs'); - -describe('test move asset', () => { - let blockchain; - let eosioToken; - let atomicassets; - let user1; - let user2; - let user3; - - beforeAll(async () => { - blockchain = new Blockchain(); - atomicassets = blockchain.createContract( - 'atomicassets', - './build/atomicassets' - ); - eosioToken = blockchain.createAccount({ - name: Name.from('eosio.token'), - wasm: fs.readFileSync('./tests/fixtures/eosio.token/eosio.token.wasm'), - abi: fs.readFileSync('./tests/fixtures/eosio.token/eosio.token.abi', 'utf8'), - }); - user1 = blockchain.createAccount('user1'); - user2 = blockchain.createAccount('user2'); - user3 = blockchain.createAccount('user3'); - }); - - beforeEach(async () => { - blockchain.resetTables(); - await atomicassets.actions.init([]).send(`${atomicassets.name.toString()}@active`); - await mintTokens(eosioToken, 'WAX', 8, 1000000000, 10000, [user1, user2, user3]); - await mintTokens(eosioToken, 'EOS', 4, 1000000000, 10000, [user1, user2, user3]); - - await atomicassets.actions.createcol([ - user1.name.toString(), - "testcollect1", - true, - [user1.name.toString()], - [], - 0.05, - [] - ]).send(`${user1.name.toString()}@active`); - - await atomicassets.actions.createschema([ - user1.name.toString(), - "testcollect1", - "testschema", - [ - {name: "name", type: "string"}, - {name: "level", type: "uint32"}, - {name: "img", type: "ipfs"} - ] - ]).send(`${user1.name.toString()}@active`); - }); - - test("throw if missing owner permission", async () => { - await expect(atomicassets.actions.move([ - user1.name.toString(), - user1.name.toString(), - user2.name.toString(), - ["1099511627776"], - '' - ]).send(`${user2.name.toString()}@active`)).rejects.toThrow('missing required authority user1'); - }); - - test("throw if from account does not exist", async () => { - await expect(atomicassets.actions.move([ - user1.name.toString(), - "nonexistent", - user2.name.toString(), - ["1099511627776"], - '' - ]).send(`${user1.name.toString()}@active`)).rejects.toThrow('from account does not exist'); - }); - - test("throw if to account does not exist", async () => { - await expect(atomicassets.actions.move([ - user1.name.toString(), - user1.name.toString(), - "nonexistent", - ["1099511627776"], - '' - ]).send(`${user1.name.toString()}@active`)).rejects.toThrow('to account does not exist'); - }); - - test("throw if from and to are the same", async () => { - await expect(atomicassets.actions.move([ - user1.name.toString(), - user1.name.toString(), - user1.name.toString(), - ["1099511627776"], - '' - ]).send(`${user1.name.toString()}@active`)).rejects.toThrow('from & to fields cannot be the same'); - }); - - test("throw if asset_ids is empty", async () => { - await expect(atomicassets.actions.move([ - user1.name.toString(), - user1.name.toString(), - user2.name.toString(), - [], - '' - ]).send(`${user1.name.toString()}@active`)).rejects.toThrow('asset_ids needs to contain at least one id'); - }); - - test("throw if memo is too long", async () => { - const longMemo = 'a'.repeat(257); // 257 characters > 256 limit - await expect(atomicassets.actions.move([ - user1.name.toString(), - user1.name.toString(), - user2.name.toString(), - ["1099511627776"], - longMemo - ]).send(`${user1.name.toString()}@active`)).rejects.toThrow('A move memo can only be 256 characters max'); - }); - - test("throw if duplicate asset IDs provided", async () => { - await expect(atomicassets.actions.move([ - user1.name.toString(), - user1.name.toString(), - user2.name.toString(), - ["1099511627776", "1099511627776"], - '' - ]).send(`${user1.name.toString()}@active`)).rejects.toThrow("Can't move the same asset multiple times"); - }); - - test("throw if owner doesn't own the asset", async () => { - // Create template and mint asset to user2 - await atomicassets.actions.createtempl([ - user1.name.toString(), - "testcollect1", - "testschema", - true, // transferable - true, // burnable - 0, // max_supply (unlimited) - [] - ]).send(`${user1.name.toString()}@active`); - - await atomicassets.actions.mintasset([ - user1.name.toString(), - "testcollect1", - "testschema", - 1, - user2.name.toString(), // mint to user2 - [], // immutable_data - [], // mutable_data - [] // tokens_to_back - ]).send(`${user1.name.toString()}@active`); - - // user1 tries to move asset they don't own - await expect(atomicassets.actions.move([ - user1.name.toString(), // user1 claims ownership - user2.name.toString(), // from user2 - user3.name.toString(), // to user3 - ["1099511627776"], - '' - ]).send(`${user1.name.toString()}@active`)).rejects.toThrow("Owner doesn't own at least one of the provided assets"); - }); - - test("throw if asset is not transferable", async () => { - // Create non-transferable template and mint asset - await atomicassets.actions.createtempl([ - user1.name.toString(), - "testcollect1", - "testschema", - false, // not transferable - true, // burnable - 0, // max_supply (unlimited) - [] - ]).send(`${user1.name.toString()}@active`); - - await atomicassets.actions.mintasset([ - user1.name.toString(), - "testcollect1", - "testschema", - 1, // template_id: 1 (non-transferable) - user1.name.toString(), - [], // immutable_data - [], // mutable_data - [] // tokens_to_back - ]).send(`${user1.name.toString()}@active`); - - await expect(atomicassets.actions.move([ - user1.name.toString(), - user1.name.toString(), - user2.name.toString(), - ["1099511627776"], // asset_id of first minted asset - '' - ]).send(`${user1.name.toString()}@active`)).rejects.toThrow("At least one asset isn't transferable"); - }); - - test("throw if holder constraint violated", async () => { - // Create template and mint asset - await atomicassets.actions.createtempl([ - user1.name.toString(), - "testcollect1", - "testschema", - true, // transferable - true, // burnable - 0, // max_supply (unlimited) - [] - ]).send(`${user1.name.toString()}@active`); - - await atomicassets.actions.mintasset([ - user1.name.toString(), - "testcollect1", - "testschema", - 1, // template_id: 1 - user1.name.toString(), - [], // immutable_data - [], // mutable_data - [] // tokens_to_back - ]).send(`${user1.name.toString()}@active`); - - // First move from owner (user1) to holder (user2) - await atomicassets.actions.move([ - user1.name.toString(), // owner - user1.name.toString(), // from (owner) - user2.name.toString(), // to (new holder) - ["1099511627776"], - 'Initial move to holder' - ]).send(`${user1.name.toString()}@active`); - - // Try to move from wrong holder (user3 instead of user2) - await expect(atomicassets.actions.move([ - user1.name.toString(), // owner - user3.name.toString(), // from (wrong holder) - user1.name.toString(), // to (back to owner) - ["1099511627776"], - 'Wrong holder attempt' - ]).send(`${user1.name.toString()}@active`)).rejects.toThrow("At least one asset invalidates the 'from:holder' constraint"); - }); - - test("successfully move asset from owner to holder", async () => { - // Create template and mint asset - await atomicassets.actions.createtempl([ - user1.name.toString(), - "testcollect1", - "testschema", - true, // transferable - true, // burnable - 0, // max_supply (unlimited) - [] - ]).send(`${user1.name.toString()}@active`); - - await atomicassets.actions.mintasset([ - user1.name.toString(), - "testcollect1", - "testschema", - 1, // template_id: 1 - user1.name.toString(), - [], // immutable_data - [], // mutable_data - [] // tokens_to_back - ]).send(`${user1.name.toString()}@active`); - - // Move from owner to holder - await expect(atomicassets.actions.move([ - user1.name.toString(), // owner - user1.name.toString(), // from (owner) - user2.name.toString(), // to (new holder) - ["1099511627776"], - 'Move to holder' - ]).send(`${user1.name.toString()}@active`)).resolves.not.toThrow(); - - // Verify holder entry was created - const holdersTable = atomicassets.tables.holders(nameToBigInt(atomicassets.name)); - const holderEntry = holdersTable.getTableRow('1099511627776'); - expect(holderEntry).toBeDefined(); - expect(holderEntry.owner).toBe(user1.name.toString()); - expect(holderEntry.holder).toBe(user2.name.toString()); - - const expectLogmoveAction = blockchain.executionTraces[1]; - expect(expectLogmoveAction.contract.toString()).toBe(atomicassets.name.toString()); - expect(expectLogmoveAction.action.toString()).toBe('logmove'); - expect(expectLogmoveAction.data.collection_name.toString()).toBe('testcollect1'); - expect(expectLogmoveAction.data.owner.toString()).toBe(user1.name.toString()); - expect(expectLogmoveAction.data.from.toString()).toBe(user1.name.toString()); - expect(expectLogmoveAction.data.to.toString()).toBe(user2.name.toString()); - expect(expectLogmoveAction.data.asset_ids.length).toBe(1); - expect(expectLogmoveAction.data.asset_ids[0].toString()).toBe("1099511627776"); - expect(expectLogmoveAction.data.memo).toBe('Move to holder'); - }); - - test("successfully move asset between holders", async () => { - // Create template and mint asset - await atomicassets.actions.createtempl([ - user1.name.toString(), - "testcollect1", - "testschema", - true, // transferable - true, // burnable - 0, // max_supply (unlimited) - [] - ]).send(`${user1.name.toString()}@active`); - - await atomicassets.actions.mintasset([ - user1.name.toString(), - "testcollect1", - "testschema", - 1, // template_id: 1 - user1.name.toString(), - [], // immutable_data - [], // mutable_data - [] // tokens_to_back - ]).send(`${user1.name.toString()}@active`); - - // First move from owner to holder - await atomicassets.actions.move([ - user1.name.toString(), // owner - user1.name.toString(), // from (owner) - user2.name.toString(), // to (new holder) - ["1099511627776"], - 'Initial move to holder' - ]).send(`${user1.name.toString()}@active`); - - // Move between holders - await expect(atomicassets.actions.move([ - user1.name.toString(), // owner - user2.name.toString(), // from (current holder) - user3.name.toString(), // to (new holder) - ["1099511627776"], - 'Move between holders' - ]).send(`${user1.name.toString()}@active`)).resolves.not.toThrow(); - - // Verify holder entry was updated - const holdersTable = atomicassets.tables.holders(nameToBigInt(atomicassets.name)); - const holderEntry = holdersTable.getTableRow('1099511627776'); - expect(holderEntry).toBeDefined(); - expect(holderEntry.owner).toBe(user1.name.toString()); - expect(holderEntry.holder).toBe(user3.name.toString()); - }); - - test("successfully move asset from holder back to owner", async () => { - // Create template and mint asset - await atomicassets.actions.createtempl([ - user1.name.toString(), - "testcollect1", - "testschema", - true, // transferable - true, // burnable - 0, // max_supply (unlimited) - [] - ]).send(`${user1.name.toString()}@active`); - - await atomicassets.actions.mintasset([ - user1.name.toString(), - "testcollect1", - "testschema", - 1, // template_id: 1 - user1.name.toString(), - [], // immutable_data - [], // mutable_data - [] // tokens_to_back - ]).send(`${user1.name.toString()}@active`); - - // First move from owner to holder - await atomicassets.actions.move([ - user1.name.toString(), // owner - user1.name.toString(), // from (owner) - user2.name.toString(), // to (new holder) - ["1099511627776"], - 'Initial move to holder' - ]).send(`${user1.name.toString()}@active`); - - // Move back to owner - await expect(atomicassets.actions.move([ - user1.name.toString(), // owner - user2.name.toString(), // from (current holder) - user1.name.toString(), // to (back to owner) - ["1099511627776"], - 'Return to owner' - ]).send(`${user1.name.toString()}@active`)).resolves.not.toThrow(); - - // Verify holder entry was deleted - const holdersTable = atomicassets.tables.holders(nameToBigInt(atomicassets.name)); - const holderEntry = holdersTable.getTableRow('1099511627776'); - expect(holderEntry).toBeUndefined(); - }); - - test("successfully move multiple assets", async () => { - // Create template and mint multiple assets - await atomicassets.actions.createtempl([ - user1.name.toString(), - "testcollect1", - "testschema", - true, // transferable - true, // burnable - 0, // max_supply (unlimited) - [] - ]).send(`${user1.name.toString()}@active`); - - await atomicassets.actions.mintasset([ - user1.name.toString(), - "testcollect1", - "testschema", - 1, // template_id: 1 - user1.name.toString(), - [], // immutable_data - [], // mutable_data - [] // tokens_to_back - ]).send(`${user1.name.toString()}@active`); - - await atomicassets.actions.mintasset([ - user1.name.toString(), - "testcollect1", - "testschema", - 1, // template_id: 1 - user1.name.toString(), - [], // immutable_data - [], // mutable_data - [] // tokens_to_back - ]).send(`${user1.name.toString()}@active`); - - // Move multiple assets - await expect(atomicassets.actions.move([ - user1.name.toString(), // owner - user1.name.toString(), // from (owner) - user2.name.toString(), // to (new holder) - ["1099511627776", "1099511627777"], // multiple assets - 'Move multiple assets' - ]).send(`${user1.name.toString()}@active`)).resolves.not.toThrow(); - - // Verify both holder entries were created - const holdersTable = atomicassets.tables.holders(nameToBigInt(atomicassets.name)); - const holderEntry1 = holdersTable.getTableRow('1099511627776'); - const holderEntry2 = holdersTable.getTableRow('1099511627777'); - - expect(holderEntry1).toBeDefined(); - expect(holderEntry1.owner).toBe(user1.name.toString()); - expect(holderEntry1.holder).toBe(user2.name.toString()); - - expect(holderEntry2).toBeDefined(); - expect(holderEntry2.owner).toBe(user1.name.toString()); - expect(holderEntry2.holder).toBe(user2.name.toString()); - }); - - test("throw if only owner can move from owner position", async () => { - // Create template and mint asset - await atomicassets.actions.createtempl([ - user1.name.toString(), - "testcollect1", - "testschema", - true, // transferable - true, // burnable - 0, // max_supply (unlimited) - [] - ]).send(`${user1.name.toString()}@active`); - - await atomicassets.actions.mintasset([ - user1.name.toString(), - "testcollect1", - "testschema", - 1, // template_id: 1 - user1.name.toString(), - [], // immutable_data - [], // mutable_data - [] // tokens_to_back - ]).send(`${user1.name.toString()}@active`); - - // user2 tries to move asset from user1 (owner) but user2 is not the owner - await expect(atomicassets.actions.move([ - user1.name.toString(), - user2.name.toString(), // should be user1 - user3.name.toString(), // to - ["1099511627776"], - 'Unauthorized move attempt' - ]).send(`${user1.name.toString()}@active`)).rejects.toThrow("Only the owner can move this asset"); - }); - - test("accept memo up to 256 characters", async () => { - // Create template and mint asset - await atomicassets.actions.createtempl([ - user1.name.toString(), - "testcollect1", - "testschema", - true, // transferable - true, // burnable - 0, // max_supply (unlimited) - [] - ]).send(`${user1.name.toString()}@active`); - - await atomicassets.actions.mintasset([ - user1.name.toString(), - "testcollect1", - "testschema", - 1, // template_id: 1 - user1.name.toString(), - [], // immutable_data - [], // mutable_data - [] // tokens_to_back - ]).send(`${user1.name.toString()}@active`); - - const validMemo = 'a'.repeat(256); // Exactly 256 characters - await expect(atomicassets.actions.move([ - user1.name.toString(), - user1.name.toString(), - user2.name.toString(), - ["1099511627776"], - validMemo - ]).send(`${user1.name.toString()}@active`)).resolves.not.toThrow(); - }); -}); \ No newline at end of file diff --git a/tests/asset-actions/renting-invariants.test.js b/tests/asset-actions/renting-invariants.test.js deleted file mode 100644 index 4111f4e..0000000 --- a/tests/asset-actions/renting-invariants.test.js +++ /dev/null @@ -1,200 +0,0 @@ -const { Blockchain, nameToBigInt, mintTokens } = require("@vaulta/vert"); -const { Name } = require('@wharfkit/antelope'); -const fs = require('fs'); - -// GAP-FILL (audit: A-BURN-RENTED, A-XFER-RENTED). The `move` action records a -// "holdership" (a holders row: asset_id + owner + holder) without locking the -// underlying asset. These tests LOCK the CURRENT on-chain behavior of what -// happens to a rented (held) asset when the OWNER burns or transfers it out -// from under the holder, so the pre-mainnet invariant decision is -// regression-guarded. They are characterization tests: they assert what the -// contract does today, not what it ideally should do. -// -// Current behavior (atomicassets.cpp): -// burnasset: holders row for the asset is ERASED, asset is burned. The -// holder silently loses the asset; no guard prevents this. -// internal_transfer: if `to` == holder, the holders row is ERASED (rental -// effectively settles to the holder). Otherwise the holders -// row's `owner` is REWRITTEN to the new owner and the -// holdership PERSISTS across the transfer. -describe("renting invariants characterization (burn / transfer of a held asset)", () => { - let blockchain; - let atomicassets; - let eosioToken; - let owner; // asset owner / lessor - let holder; // current holder / lessee - let third; // unrelated third party - - beforeAll(async () => { - blockchain = new Blockchain(); - atomicassets = blockchain.createContract( - 'atomicassets', - './build/atomicassets' - ); - eosioToken = blockchain.createAccount({ - name: Name.from('eosio.token'), - wasm: fs.readFileSync('./tests/fixtures/eosio.token/eosio.token.wasm'), - abi: fs.readFileSync('./tests/fixtures/eosio.token/eosio.token.abi', 'utf8'), - }); - owner = blockchain.createAccount('user1'); - holder = blockchain.createAccount('user2'); - third = blockchain.createAccount('user3'); - }); - - beforeEach(async () => { - blockchain.resetTables(); - await atomicassets.actions.init([]).send(`${atomicassets.name.toString()}@active`); - await mintTokens(eosioToken, 'WAX', 8, 1000000000, 10000, [owner, holder, third]); - - await atomicassets.actions.createcol([ - owner.name.toString(), - "testcollect1", - true, - [owner.name.toString()], - [], - 0.05, - [] - ]).send(`${owner.name.toString()}@active`); - - await atomicassets.actions.createschema([ - owner.name.toString(), - "testcollect1", - "testschema", - [ - {name: "name", type: "string"}, - {name: "level", type: "uint32"}, - {name: "img", type: "ipfs"} - ] - ]).send(`${owner.name.toString()}@active`); - - // Transferable + burnable template so move/transfer/burn are all allowed. - await atomicassets.actions.createtempl([ - owner.name.toString(), - "testcollect1", - "testschema", - true, // transferable - true, // burnable - 0, // max_supply - [] - ]).send(`${owner.name.toString()}@active`); - }); - - // Mints one asset to `owner` and moves it out to `holder`, creating the - // holders row. Returns the asset_id. - async function mintAndRent() { - await atomicassets.actions.mintasset([ - owner.name.toString(), - "testcollect1", - "testschema", - 1, - owner.name.toString(), - [], - [], - [] - ]).send(`${owner.name.toString()}@active`); - - const assetId = "1099511627776"; - - await atomicassets.actions.move([ - owner.name.toString(), // owner - owner.name.toString(), // from (owner) - holder.name.toString(), // to (new holder) - [assetId], - 'Rent out asset' - ]).send(`${owner.name.toString()}@active`); - - // Holders row exists, owner still owns the asset row. - const holders = atomicassets.tables.holders(nameToBigInt(atomicassets.name)).getTableRows(); - expect(holders).toHaveLength(1); - expect(holders[0]).toMatchObject({ - asset_id: assetId, - owner: owner.name.toString(), - holder: holder.name.toString() - }); - - return assetId; - } - - // A-BURN-RENTED: the OWNER can burn an asset that is currently held out by a - // lessee. There is NO guard. The asset is burned and the holders row is - // erased; the holder is left with nothing. - test("CURRENT BEHAVIOR: owner can burn a rented-out asset (holder loses it)", async () => { - const assetId = await mintAndRent(); - - // Owner burns the held asset (no rejection). - await expect(atomicassets.actions.burnasset([ - owner.name.toString(), - assetId - ]).send(`${owner.name.toString()}@active`)).resolves.not.toThrow(); - - // Asset is gone from the owner's scope. - const ownerAssets = atomicassets.tables.assets(nameToBigInt(owner.name)).getTableRows(); - expect(ownerAssets).toEqual([]); - - // Holder never had an asset row in their scope (move only records - // holdership, it does not move the asset row). - const holderAssets = atomicassets.tables.assets(nameToBigInt(holder.name)).getTableRows(); - expect(holderAssets).toEqual([]); - - // Holders row was erased by the burn. - const holders = atomicassets.tables.holders(nameToBigInt(atomicassets.name)).getTableRows(); - expect(holders).toEqual([]); - }); - - // A-XFER-RENTED (transfer to an unrelated third party, NOT the holder): - // the OWNER can transfer a held-out asset to someone else. The holders row - // is NOT erased; instead its `owner` field is rewritten to the new owner and - // the holdership PERSISTS. The asset row moves to the new owner's scope. - test("CURRENT BEHAVIOR: owner transfers a rented-out asset to a third party (holdership persists, owner rewritten)", async () => { - const assetId = await mintAndRent(); - - // Owner transfers the held asset to `third` (not the holder). - await expect(atomicassets.actions.transfer([ - owner.name.toString(), - third.name.toString(), - [assetId], - 'Sell rented asset out from under holder' - ]).send(`${owner.name.toString()}@active`)).resolves.not.toThrow(); - - // Asset row moved owner -> third. - const ownerAssets = atomicassets.tables.assets(nameToBigInt(owner.name)).getTableRows(); - expect(ownerAssets).toEqual([]); - const thirdAssets = atomicassets.tables.assets(nameToBigInt(third.name)).getTableRows(); - expect(thirdAssets).toHaveLength(1); - expect(thirdAssets[0]).toMatchObject({ asset_id: assetId }); - - // Holders row PERSISTS; owner rewritten to `third`, holder unchanged. - const holders = atomicassets.tables.holders(nameToBigInt(atomicassets.name)).getTableRows(); - expect(holders).toHaveLength(1); - expect(holders[0]).toMatchObject({ - asset_id: assetId, - owner: third.name.toString(), - holder: holder.name.toString() - }); - }); - - // A-XFER-RENTED (transfer TO the current holder): the rental "settles", the - // holders row is erased and the asset row moves to the holder, who now owns - // it outright. - test("CURRENT BEHAVIOR: owner transfers a rented-out asset to the holder (holdership settles)", async () => { - const assetId = await mintAndRent(); - - await expect(atomicassets.actions.transfer([ - owner.name.toString(), - holder.name.toString(), - [assetId], - 'Settle rental to holder' - ]).send(`${owner.name.toString()}@active`)).resolves.not.toThrow(); - - // Asset row moved owner -> holder. - const ownerAssets = atomicassets.tables.assets(nameToBigInt(owner.name)).getTableRows(); - expect(ownerAssets).toEqual([]); - const holderAssets = atomicassets.tables.assets(nameToBigInt(holder.name)).getTableRows(); - expect(holderAssets).toHaveLength(1); - expect(holderAssets[0]).toMatchObject({ asset_id: assetId }); - - // Holders row erased (rental settled to holder). - const holders = atomicassets.tables.holders(nameToBigInt(atomicassets.name)).getTableRows(); - expect(holders).toEqual([]); - }); -}); diff --git a/tests/deposit-withdraw-back-burn-actions/burnasset.test.js b/tests/deposit-withdraw-back-burn-actions/burnasset.test.js index 07ce195..6270aeb 100644 --- a/tests/deposit-withdraw-back-burn-actions/burnasset.test.js +++ b/tests/deposit-withdraw-back-burn-actions/burnasset.test.js @@ -187,48 +187,4 @@ describe("test burnasset contract", () => { "1099511627776" ]).send(`${user2.name.toString()}@active`)).rejects.toThrow("missing required authority"); }); - - test("burn asset with holder record deletes the holder entry", async () => { - expect.assertions(3); - - // Mint asset for user1 - await atomicassets.actions.mintasset([ - user1.name.toString(), - "testcollect1", - "testschema", - -1, - user1.name.toString(), - [], - [], - [] - ]).send(`${user1.name.toString()}@active`); - - // Move asset from owner (user1) to holder (user2) - await atomicassets.actions.move([ - user1.name.toString(), // owner - user1.name.toString(), // from (owner) - user2.name.toString(), // to (new holder) - ["1099511627776"], - 'Move to holder for burning test' - ]).send(`${user1.name.toString()}@owner`); - - // Verify holder record exists - let holders = atomicassets.tables.holders(nameToBigInt(atomicassets.name)).getTableRows(); - expect(holders).toHaveLength(1); - expect(holders[0]).toMatchObject({ - asset_id: "1099511627776", - owner: user1.name.toString(), - holder: user2.name.toString() - }); - - // Burn the asset (owner can burn even when held by someone else) - await atomicassets.actions.burnasset([ - user1.name.toString(), - "1099511627776" - ]).send(`${user1.name.toString()}@active`); - - // Verify holder record was deleted along with the asset - holders = atomicassets.tables.holders(nameToBigInt(atomicassets.name)).getTableRows(); - expect(holders).toHaveLength(0); - }); -}); \ No newline at end of file +}); diff --git a/tests/transfer-offer-actions/transfer.test.js b/tests/transfer-offer-actions/transfer.test.js index c7d830f..baa62da 100644 --- a/tests/transfer-offer-actions/transfer.test.js +++ b/tests/transfer-offer-actions/transfer.test.js @@ -528,211 +528,4 @@ describe('test transfer contract', () => { "" ]).send(`${user2.name.toString()}@active`)).rejects.toThrow("missing required authority"); }); - - test("transfer asset with holder record - transfer to holder deletes holder entry", async () => { - // Mint asset for user1 - await atomicassets.actions.mintasset([ - user1.name.toString(), - "testcollect1", - "testschema", - -1, - user1.name.toString(), - [], - [], - [] - ]).send(`${user1.name.toString()}@active`); - - // Move asset from owner (user1) to holder (user2) using move action - await atomicassets.actions.move([ - user1.name.toString(), // owner - user1.name.toString(), // from (owner) - user2.name.toString(), // to (new holder) - ["1099511627776"], - 'Create holder relationship for transfer test' - ]).send(`${user1.name.toString()}@owner`); - - // Verify holder record exists - let holders = atomicassets.tables.holders(nameToBigInt(atomicassets.name)).getTableRows(); - expect(holders).toHaveLength(1); - expect(holders[0]).toMatchObject({ - asset_id: "1099511627776", - owner: user1.name.toString(), - holder: user2.name.toString() - }); - - // Transfer asset from owner (user1) to the current holder (user2) - // This should delete the holder record since we're transferring to the holder - await atomicassets.actions.transfer([ - user1.name.toString(), // from (owner) - user2.name.toString(), // to (current holder) - ["1099511627776"], - "Transfer to current holder" - ]).send(`${user1.name.toString()}@active`); - - // Verify holder record was deleted - holders = atomicassets.tables.holders(nameToBigInt(atomicassets.name)).getTableRows(); - expect(holders).toHaveLength(0); - - // Verify asset is now owned by user2 - const user2_assets = atomicassets.tables.assets(nameToBigInt(user2.name)).getTableRows(); - expect(user2_assets).toHaveLength(1); - expect(user2_assets[0].asset_id).toBe("1099511627776"); - }); - - test("transfer asset with holder record - transfer to new owner updates holder ownership", async () => { - const user3 = blockchain.createAccount('user3'); - - // Mint asset for user1 - await atomicassets.actions.mintasset([ - user1.name.toString(), - "testcollect1", - "testschema", - -1, - user1.name.toString(), - [], - [], - [] - ]).send(`${user1.name.toString()}@active`); - - // Move asset from owner (user1) to holder (user2) - await atomicassets.actions.move([ - user1.name.toString(), // owner - user1.name.toString(), // from (owner) - user2.name.toString(), // to (new holder) - ["1099511627776"], - 'Create holder relationship for transfer test' - ]).send(`${user1.name.toString()}@owner`); - - // Verify initial holder record - let holders = atomicassets.tables.holders(nameToBigInt(atomicassets.name)).getTableRows(); - expect(holders).toHaveLength(1); - expect(holders[0]).toMatchObject({ - asset_id: "1099511627776", - owner: user1.name.toString(), - holder: user2.name.toString() - }); - - // Transfer asset from owner (user1) to new owner (user3) - // This should update the holder record to show user3 as the new owner - await atomicassets.actions.transfer([ - user1.name.toString(), // from (current owner) - user3.name.toString(), // to (new owner) - ["1099511627776"], - "Transfer to new owner while held by someone else" - ]).send(`${user1.name.toString()}@active`); - - // Verify holder record was updated with new ownership - holders = atomicassets.tables.holders(nameToBigInt(atomicassets.name)).getTableRows(); - expect(holders).toHaveLength(1); - expect(holders[0]).toMatchObject({ - asset_id: "1099511627776", - owner: user3.name.toString(), // updated to new owner - holder: user2.name.toString() // holder remains the same - }); - - // Verify asset is now owned by user3 - const user3_assets = atomicassets.tables.assets(nameToBigInt(user3.name)).getTableRows(); - expect(user3_assets).toHaveLength(1); - expect(user3_assets[0].asset_id).toBe("1099511627776"); - }); - - test("transfer asset without holder record - no holder table interactions", async () => { - // Mint asset for user1 (no holder relationship created) - await atomicassets.actions.mintasset([ - user1.name.toString(), - "testcollect1", - "testschema", - -1, - user1.name.toString(), - [], - [], - [] - ]).send(`${user1.name.toString()}@active`); - - // Verify no holder records exist initially - let holders = atomicassets.tables.holders(nameToBigInt(atomicassets.name)).getTableRows(); - expect(holders).toHaveLength(0); - - // Transfer asset normally (owner to new owner, no holder involved) - await atomicassets.actions.transfer([ - user1.name.toString(), - user2.name.toString(), - ["1099511627776"], - "Normal transfer without holder" - ]).send(`${user1.name.toString()}@active`); - - // Verify still no holder records (normal transfer case) - holders = atomicassets.tables.holders(nameToBigInt(atomicassets.name)).getTableRows(); - expect(holders).toHaveLength(0); - - // Verify asset was transferred successfully - const user2_assets = atomicassets.tables.assets(nameToBigInt(user2.name)).getTableRows(); - expect(user2_assets).toHaveLength(1); - expect(user2_assets[0].asset_id).toBe("1099511627776"); - }); - - test("transfer multiple assets with mixed holder scenarios", async () => { - const user3 = blockchain.createAccount('user3'); - - // Mint two assets for user1 - await atomicassets.actions.mintasset([ - user1.name.toString(), - "testcollect1", - "testschema", - -1, - user1.name.toString(), - [], - [], - [] - ]).send(`${user1.name.toString()}@active`); - - await atomicassets.actions.mintasset([ - user1.name.toString(), - "testcollect1", - "testschema", - -1, - user1.name.toString(), - [], - [], - [] - ]).send(`${user1.name.toString()}@active`); - - // Create holder relationship for first asset only - await atomicassets.actions.move([ - user1.name.toString(), // owner - user1.name.toString(), // from (owner) - user2.name.toString(), // to (new holder) - ["1099511627776"], // only first asset - 'Create holder for first asset only' - ]).send(`${user1.name.toString()}@owner`); - - // Verify only one holder record exists - let holders = atomicassets.tables.holders(nameToBigInt(atomicassets.name)).getTableRows(); - expect(holders).toHaveLength(1); - expect(holders[0].asset_id).toBe("1099511627776"); - - // Transfer both assets to user3 - // First asset has holder (should update ownership) - // Second asset has no holder (normal transfer) - await atomicassets.actions.transfer([ - user1.name.toString(), - user3.name.toString(), - ["1099511627776", "1099511627777"], - "Transfer assets with mixed holder scenarios" - ]).send(`${user1.name.toString()}@active`); - - // Verify holder record was updated for first asset - holders = atomicassets.tables.holders(nameToBigInt(atomicassets.name)).getTableRows(); - expect(holders).toHaveLength(1); - expect(holders[0]).toMatchObject({ - asset_id: "1099511627776", - owner: user3.name.toString(), // ownership updated - holder: user2.name.toString() // holder unchanged - }); - - // Verify both assets are now owned by user3 - const user3_assets = atomicassets.tables.assets(nameToBigInt(user3.name)).getTableRows(); - expect(user3_assets).toHaveLength(2); - expect(user3_assets.map(a => a.asset_id).sort()).toEqual(["1099511627776", "1099511627777"]); - }); -}); \ No newline at end of file +});