diff --git a/.github/workflows/test.yml b/.github/workflows/ci.yml similarity index 62% rename from .github/workflows/test.yml rename to .github/workflows/ci.yml index b2f52c9b..4227c071 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/ci.yml @@ -1,9 +1,10 @@ -name: Test +name: Origami CI on: [push, pull_request] env: - DOJO_VERSION: v0.3.3 + DOJO_VERSION: v0.3.4 + SCARB_VERSION: v2.3.1 jobs: check: @@ -12,11 +13,14 @@ jobs: steps: - uses: actions/checkout@v4 - uses: software-mansion/setup-scarb@v1 + with: + scarb-version: ${{ env.SCARB_VERSION }} - name: Format - run: scarb fmt --package random --check + run: scarb fmt --check shell: bash build: + needs: check runs-on: ubuntu-latest name: Build package steps: @@ -25,8 +29,9 @@ jobs: - name: Build run: sozo build shell: bash - + algebra: + needs: [check, build] runs-on: ubuntu-latest name: Test algebra crate steps: @@ -37,6 +42,7 @@ jobs: shell: bash defi: + needs: [check, build] runs-on: ubuntu-latest name: Test defi crate steps: @@ -47,6 +53,7 @@ jobs: shell: bash random: + needs: [check, build] runs-on: ubuntu-latest name: Test random crate steps: @@ -57,6 +64,7 @@ jobs: shell: bash security: + needs: [check, build] runs-on: ubuntu-latest name: Test security crate steps: @@ -64,4 +72,26 @@ jobs: - uses: ./.github/actions/setup - name: Test run: sozo test -f security + shell: bash + + market: + needs: [check, build] + runs-on: ubuntu-latest + name: Test market example + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/setup + - name: Test + run: sozo test -f market + shell: bash + + projectile: + needs: [check, build] + runs-on: ubuntu-latest + name: Test projectile example + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/setup + - name: Test + run: sozo test -f projectile shell: bash \ No newline at end of file diff --git a/Scarb.lock b/Scarb.lock index 508ef783..052220f8 100644 --- a/Scarb.lock +++ b/Scarb.lock @@ -22,15 +22,31 @@ dependencies = [ [[package]] name = "dojo" -version = "0.3.3" -source = "git+https://github.com/dojoengine/dojo.git?tag=v0.3.3#3c9f109e667ca5d12739e6553fdb8261378f4ecf" +version = "0.3.4" +source = "git+https://github.com/dojoengine/dojo.git?tag=v0.3.4#a3140d88b08b79c5ff2261c1db81bafe80b5cc91" dependencies = [ "dojo_plugin", ] [[package]] name = "dojo_plugin" -version = "0.3.3" +version = "0.3.4" + +[[package]] +name = "market" +version = "0.0.0" +dependencies = [ + "cubit", + "dojo", +] + +[[package]] +name = "projectile" +version = "0.0.0" +dependencies = [ + "algebra", + "cubit", +] [[package]] name = "random" diff --git a/Scarb.toml b/Scarb.toml index c32f1f57..bdb31d56 100644 --- a/Scarb.toml +++ b/Scarb.toml @@ -4,11 +4,9 @@ version = "0.0.0" description = "Community-maintained libraries for Cairo" homepage = "https://github.com/dojoengine/origami" members = [ - "crates/algebra", - "crates/defi", - "crates/random", - "crates/security", + "crates/*", + "examples/*", ] [workspace.dependencies] -dojo = { git = "https://github.com/dojoengine/dojo.git", tag = "v0.3.3" } \ No newline at end of file +dojo = { git = "https://github.com/dojoengine/dojo.git", tag = "v0.3.4" } \ No newline at end of file diff --git a/crates/algebra/src/lib.cairo b/crates/algebra/src/lib.cairo index 46a1516d..347476c3 100644 --- a/crates/algebra/src/lib.cairo +++ b/crates/algebra/src/lib.cairo @@ -1,3 +1,3 @@ mod vec2; mod vector; -mod matrix; \ No newline at end of file +mod matrix; diff --git a/crates/algebra/src/matrix.cairo b/crates/algebra/src/matrix.cairo index 6612f9d1..5ed219d9 100644 --- a/crates/algebra/src/matrix.cairo +++ b/crates/algebra/src/matrix.cairo @@ -412,4 +412,4 @@ mod tests { assert(inverse.get(2, 1) == -1, 'Matrix: inversion failed'); assert(inverse.get(2, 2) == 1, 'Matrix: inversion failed'); } -} \ No newline at end of file +} diff --git a/crates/algebra/src/vec2.cairo b/crates/algebra/src/vec2.cairo index 116ef2f1..6bee5980 100644 --- a/crates/algebra/src/vec2.cairo +++ b/crates/algebra/src/vec2.cairo @@ -78,22 +78,22 @@ impl Vec2Impl, impl TDrop: Drop> of Vec2Trait { /// Vec2 -> Vec2 #[inline(always)] fn xx(self: Vec2) -> Vec2 { - Vec2 { x: self.x, y: self.x, } + Vec2 { x: self.x, y: self.x, } } #[inline(always)] fn xy(self: Vec2) -> Vec2 { - Vec2 { x: self.x, y: self.y, } + Vec2 { x: self.x, y: self.y, } } #[inline(always)] fn yx(self: Vec2) -> Vec2 { - Vec2 { x: self.y, y: self.x, } + Vec2 { x: self.y, y: self.x, } } #[inline(always)] fn yy(self: Vec2) -> Vec2 { - Vec2 { x: self.y, y: self.y, } + Vec2 { x: self.y, y: self.y, } } } @@ -266,4 +266,4 @@ mod tests { assert(vec2yy.y.mag == 2 * ONE_u128, 'invalid y.mag'); assert(vec2yy.y.sign == true, 'invalid y.sign'); } -} \ No newline at end of file +} diff --git a/crates/algebra/src/vector.cairo b/crates/algebra/src/vector.cairo index 63ddb1e0..9bff622f 100644 --- a/crates/algebra/src/vector.cairo +++ b/crates/algebra/src/vector.cairo @@ -127,4 +127,4 @@ mod tests { let result = vector1.dot(vector2); assert(result == 32, 'Vector: dot product failed'); // 1*4 + 2*5 + 3*6 = 32 } -} \ No newline at end of file +} diff --git a/examples/market/.gitignore b/examples/market/.gitignore new file mode 100644 index 00000000..1de56593 --- /dev/null +++ b/examples/market/.gitignore @@ -0,0 +1 @@ +target \ No newline at end of file diff --git a/examples/market/Scarb.lock b/examples/market/Scarb.lock new file mode 100644 index 00000000..f74da0a2 --- /dev/null +++ b/examples/market/Scarb.lock @@ -0,0 +1,27 @@ +# Code generated by scarb DO NOT EDIT. +version = 1 + +[[package]] +name = "cubit" +version = "1.2.0" +source = "git+https://github.com/influenceth/cubit?rev=b459053#b4590530d5aeae9aabd36740cc2a3d9e6adc5fde" + +[[package]] +name = "dojo" +version = "0.3.3" +source = "git+https://github.com/dojoengine/dojo.git?tag=v0.3.3#3c9f109e667ca5d12739e6553fdb8261378f4ecf" +dependencies = [ + "dojo_plugin", +] + +[[package]] +name = "dojo_plugin" +version = "0.3.3" + +[[package]] +name = "market" +version = "0.0.0" +dependencies = [ + "cubit", + "dojo", +] diff --git a/examples/market/Scarb.toml b/examples/market/Scarb.toml new file mode 100644 index 00000000..5fc2f482 --- /dev/null +++ b/examples/market/Scarb.toml @@ -0,0 +1,9 @@ +[package] +name = "market" +version = "0.0.0" +description = "Example of defi crate usage." +homepage = "https://github.com/dojoengine/origami/tree/examples/market" + +[dependencies] +cubit = { git = "https://github.com/influenceth/cubit", rev = "b459053" } +dojo.workspace = true \ No newline at end of file diff --git a/examples/market/src/lib.cairo b/examples/market/src/lib.cairo new file mode 100644 index 00000000..c617df4d --- /dev/null +++ b/examples/market/src/lib.cairo @@ -0,0 +1,17 @@ +mod models { + mod cash; + mod item; + mod liquidity; + mod market; +} + +mod systems { + mod liquidity; + mod trade; +} + +#[cfg(test)] +mod tests { + mod setup; + mod trade; +} diff --git a/examples/market/src/models/cash.cairo b/examples/market/src/models/cash.cairo new file mode 100644 index 00000000..d447b317 --- /dev/null +++ b/examples/market/src/models/cash.cairo @@ -0,0 +1,10 @@ +// Starknet imports + +use starknet::ContractAddress; + +#[derive(Model, Copy, Drop, Serde)] +struct Cash { + #[key] + player: ContractAddress, + amount: u128, +} diff --git a/examples/market/src/models/item.cairo b/examples/market/src/models/item.cairo new file mode 100644 index 00000000..7e395443 --- /dev/null +++ b/examples/market/src/models/item.cairo @@ -0,0 +1,12 @@ +// Starknet imports + +use starknet::ContractAddress; + +#[derive(Model, Copy, Drop, Serde)] +struct Item { + #[key] + player: ContractAddress, + #[key] + item_id: u32, + quantity: u128, +} diff --git a/examples/market/src/models/liquidity.cairo b/examples/market/src/models/liquidity.cairo new file mode 100644 index 00000000..8e46f834 --- /dev/null +++ b/examples/market/src/models/liquidity.cairo @@ -0,0 +1,56 @@ +// Starknet imports + +use starknet::ContractAddress; + +// Dojo imports + +use dojo::database::schema::{Struct, Ty, SchemaIntrospection, Member, serialize_member}; + +// External imports + +use cubit::f128::types::fixed::Fixed; + +// Constants + +const SCALING_FACTOR: u128 = 10000; + +impl SchemaIntrospectionFixed of SchemaIntrospection { + #[inline(always)] + fn size() -> usize { + 2 + } + + #[inline(always)] + fn layout(ref layout: Array) { + layout.append(128); + layout.append(1); + } + + #[inline(always)] + fn ty() -> Ty { + Ty::Struct( + Struct { + name: 'Fixed', + attrs: array![].span(), + children: array![ + serialize_member( + @Member { name: 'mag', ty: Ty::Primitive('u128'), attrs: array![].span() } + ), + serialize_member( + @Member { name: 'sign', ty: Ty::Primitive('bool'), attrs: array![].span() } + ) + ] + .span() + } + ) + } +} + +#[derive(Model, Copy, Drop, Serde)] +struct Liquidity { + #[key] + player: ContractAddress, + #[key] + item_id: u32, + shares: Fixed, +} diff --git a/examples/market/src/models/market.cairo b/examples/market/src/models/market.cairo new file mode 100644 index 00000000..50369e11 --- /dev/null +++ b/examples/market/src/models/market.cairo @@ -0,0 +1,423 @@ +// External imports + +use cubit::f128::types::fixed::{Fixed, FixedTrait}; + +// Constants + +const SCALING_FACTOR: u128 = 10000; + +#[derive(Model, Copy, Drop, Serde)] +struct Market { + #[key] + item_id: u32, + cash_amount: u128, + item_quantity: u128, +} + +#[generate_trait] +impl MarketImpl of MarketTrait { + fn buy(self: @Market, quantity: u128) -> u128 { + assert(quantity < *self.item_quantity, 'not enough liquidity'); + let (quantity, available, cash) = normalize(quantity, self); + let k = cash * available; + let cost = (k / (available - quantity)) - cash; + cost + } + + fn sell(self: @Market, quantity: u128) -> u128 { + let (quantity, available, cash) = normalize(quantity, self); + let k = cash * available; + let payout = cash - (k / (available + quantity)); + payout + } + + // Get normalized reserve cash amount and item quantity + fn get_reserves(self: @Market) -> (u128, u128) { + let reserve_quantity: u128 = (*self.item_quantity).into() * SCALING_FACTOR; + (*self.cash_amount, reserve_quantity) + } + + // Get the liquidity of the market + // Use cubit fixed point math library to compute the square root of the product of the reserves + fn liquidity(self: @Market) -> Fixed { + // Get normalized reserve cash amount and item quantity + let (reserve_amount, reserve_quantity) = self.get_reserves(); + + // Convert reserve amount to fixed point + let reserve_amount = FixedTrait::new_unscaled(reserve_amount, false); + let reserve_quantity = FixedTrait::new_unscaled(reserve_quantity, false); + + // L = sqrt(X * Y) + (reserve_amount * reserve_quantity).sqrt() + } + + // Check if the market has liquidity + fn has_liquidity(self: @Market) -> bool { + *self.cash_amount > 0 || *self.item_quantity > 0 + } + + // Given some amount of cash, return the equivalent/optimal quantity of items + // based on the reserves in the market + fn quote_quantity(self: @Market, amount: u128) -> u128 { + assert(amount > 0, 'insufficient amount'); + assert(self.has_liquidity(), 'insufficient liquidity'); + + // Get normalized reserve cash amount and item quantity + let (reserve_amount, reserve_quantity) = self.get_reserves(); + + // Convert amount to fixed point + let amount = FixedTrait::new_unscaled(amount, false); + + // Convert reserve amount and quantity to fixed point + let reserve_amount = FixedTrait::new_unscaled(reserve_amount, false); + let reserve_quantity = FixedTrait::new_unscaled(reserve_quantity, false); + + // dy = Y * dx / X + let quantity_optimal = (reserve_quantity * amount) / reserve_amount; + + // Convert from fixed point to u128 + let res: u128 = quantity_optimal.try_into().unwrap(); + res + } + + // Given some quantity of items, return the equivalent/optimal amount of cash + // based on the reserves in the market + fn quote_amount(self: @Market, quantity: u128) -> u128 { + assert(quantity > 0, 'insufficient quantity'); + assert(self.has_liquidity(), 'insufficient liquidity'); + + // Get normalized reserve cash amount and item quantity + let (reserve_amount, reserve_quantity) = self.get_reserves(); + + // Convert reserve amount and quantity to fixed point + let reserve_amount = FixedTrait::new_unscaled(reserve_amount, false); + let reserve_quantity = FixedTrait::new_unscaled(reserve_quantity, false); + + // Normalize quantity + let quantity: u128 = quantity.into() * SCALING_FACTOR; + + // Convert quantity to fixed point + let quantity = FixedTrait::new_unscaled(quantity, false); + + // dx = X * dy / Y + let amount_optimal = (reserve_amount * quantity) / reserve_quantity; + + // Convert from fixed point to u128 + amount_optimal.try_into().unwrap() + } + + // Inner function to add liquidity to the market, computes the optimal amount and quantity + // + // Arguments: + // + // amount: The amount of cash to add to the market + // quantity: The quantity of items to add to the market + // + // Returns: + // + // (amount, quantity): The amount of cash and quantity of items added to the market + fn add_liquidity_inner(self: @Market, amount: u128, quantity: u128) -> (u128, u128) { + // If there is no liquidity, then the amount and quantity are the optimal + if !self.has_liquidity() { + // Ensure that the amount and quantity are greater than zero + assert(amount > 0, 'insufficient amount'); + assert(quantity > 0, 'insufficient quantity'); + (amount, quantity) + } else { + // Given the amount, get optimal quantity to add to the market + let quantity_optimal = self.quote_quantity(amount); + if quantity_optimal <= quantity { + // Add the given amount and optimal quantity to the market + (amount, quantity_optimal) + } else { + let amount_optimal = self.quote_amount(quantity); + // Ensure that the optimal amount is less than or equal to the given amount + assert(amount_optimal <= amount, 'insufficient amount'); + (amount_optimal, quantity) + } + } + } + + // Add liquidity to the market, mints shares for the given amount of liquidity provided + // + // Arguments: + // + // amount: The amount of cash to add to the market + // quantity: The quantity of items to add to the market + // + // Returns: + // + // (amount, quantity, shares): The amount of cash and quantity of items added to the market and the shares minted + fn add_liquidity(self: @Market, amount: u128, quantity: u128) -> (u128, u128, Fixed) { + // Compute the amount and quantity to add to the market + let (amount, quantity) = self.add_liquidity_inner(amount, quantity); + // Mint shares for the given amount of liquidity provided + let shares = self.mint_shares(amount, quantity); + (amount, quantity, shares) + } + + // Mint shares for the given amount of liquidity provided + fn mint_shares(self: @Market, amount: u128, quantity: u128) -> Fixed { + // If there is no liquidity, then mint total shares + if !self.has_liquidity() { + let quantity: u128 = quantity.into() * SCALING_FACTOR; + (FixedTrait::new_unscaled(amount, false) * FixedTrait::new_unscaled(quantity, false)) + .sqrt() + } else { + // Convert amount to fixed point + let amount = FixedTrait::new_unscaled(amount, false); + + // Get normalized reserve cash amount and item quantity + let (reserve_amount, _) = self.get_reserves(); + + // Convert reserve amount to fixed point + let reserve_amount = FixedTrait::new_unscaled(reserve_amount, false); + + // Get total liquidity + let liquidity = self.liquidity(); + + // Compute the amount of shares to mint + // S = dx * L/X = dy * L/Y + (amount * liquidity) / reserve_amount + } + } + + // Remove liquidity from the market, return the corresponding amount and quantity payout + // + // Arguments: + // + // shares: The amount of liquidity shares to remove from the market + // + // Returns: + // + // (amount, quantity): The amount of cash and quantity of items removed from the market + fn remove_liquidity(self: @Market, shares: Fixed) -> (u128, u128) { + // Ensure that the market has liquidity + let liquidity = self.liquidity(); + assert(shares <= liquidity, 'insufficient liquidity'); + + // Get normalized reserve cash amount and item quantity + let (reserve_amount, reserve_quantity) = self.get_reserves(); + + // Convert reserve amount and quantity to fixed point + let reserve_amount = FixedTrait::new_unscaled(reserve_amount, false); + let reserve_quantity = FixedTrait::new_unscaled(reserve_quantity, false); + + // Compute the amount and quantity to remove from the market + // dx = S * X / L + let amount = (shares * reserve_amount) / liquidity; + // dy = S * Y / L + let quantity = (shares * reserve_quantity) / liquidity; + + // Convert amount and quantity both from fixed point to u128 and unscaled u128, respectively + (amount.try_into().unwrap(), quantity.try_into().unwrap() / SCALING_FACTOR) + } +} + +fn normalize(quantity: u128, market: @Market) -> (u128, u128, u128) { + let quantity: u128 = quantity.into() * SCALING_FACTOR; + let available: u128 = (*market.item_quantity).into() * SCALING_FACTOR; + (quantity, available, *market.cash_amount) +} + +#[cfg(test)] +mod tests { + // Local imports + + use super::{Market, MarketTrait, SCALING_FACTOR}; + use super::{Fixed, FixedTrait}; + + // Constants + + const TOLERANCE: u128 = 18446744073709550; // 0.001 + + // Helpers + + fn assert_approx_equal(expected: Fixed, actual: Fixed, tolerance: u128) { + let left_bound = expected - FixedTrait::new(tolerance, false); + let right_bound = expected + FixedTrait::new(tolerance, false); + assert(left_bound <= actual && actual <= right_bound, 'Not approx eq'); + } + + #[test] + #[should_panic(expected: ('not enough liquidity',))] + fn test_not_enough_quantity() { + let market = Market { + item_id: 1, cash_amount: SCALING_FACTOR * 1, item_quantity: 1 + }; // pool 1:1 + let cost = market.buy(10); + } + + #[test] + #[available_gas(100000)] + fn test_market_buy() { + let market = Market { + item_id: 1, cash_amount: SCALING_FACTOR * 1, item_quantity: 10 + }; // pool 1:10 + let cost = market.buy(5); + assert(cost == SCALING_FACTOR * 1, 'wrong cost'); + } + + #[test] + #[available_gas(100000)] + fn test_market_sell() { + let market = Market { + item_id: 1, cash_amount: SCALING_FACTOR * 1, item_quantity: 10 + }; // pool 1:10 + let payout = market.sell(5); + assert(payout == 3334, 'wrong payout'); + } + + #[test] + #[available_gas(500000)] + fn test_market_add_liquidity_no_initial() { + // Without initial liquidity + let market = Market { item_id: 1, cash_amount: 0, item_quantity: 0 }; + + // Add liquidity + let (amount, quantity) = (SCALING_FACTOR * 5, 5); // pool 1:1 + let (amount_add, quantity_add, liquidity_add) = market.add_liquidity(amount, quantity); + + // Assert that the amount and quantity added are the same as the given amount and quantity + // and that the liquidity shares minted are the same as the entire liquidity + assert(amount_add == amount, 'wrong cash amount'); + assert(quantity_add == quantity, 'wrong item quantity'); + + // Convert amount and quantity to fixed point + let amount = FixedTrait::new_unscaled(amount, false); + let quantity: u128 = quantity.into() * SCALING_FACTOR; + let quantity = FixedTrait::new_unscaled(quantity, false); + assert(liquidity_add == (amount * quantity).sqrt(), 'wrong liquidity'); + } + + #[test] + #[available_gas(600000)] + fn test_market_add_liquidity_optimal() { + // With initial liquidity + let market = Market { + item_id: 1, cash_amount: SCALING_FACTOR * 1, item_quantity: 10 + }; // pool 1:10 + let initial_liquidity = market.liquidity(); + + // Add liquidity with the same ratio + let (amount, quantity) = (SCALING_FACTOR * 2, 20); // pool 1:10 + let (amount_add, quantity_add, liquidity_add) = market.add_liquidity(amount, quantity); + + // Assert + assert(amount_add == amount, 'wrong cash amount'); + assert(quantity_add == quantity, 'wrong item quantity'); + + // Get expected amount and convert to fixed point + let expected_amount = FixedTrait::new_unscaled(SCALING_FACTOR * 1 + amount, false); + let expected_quantity: u128 = (10 + quantity).into() * SCALING_FACTOR; + let expected_quantity = FixedTrait::new_unscaled(expected_quantity, false); + + // Compute the expected liquidity shares + let expected_liquidity = FixedTrait::sqrt(expected_amount * expected_quantity); + let final_liquidity = initial_liquidity + liquidity_add; + assert_approx_equal(expected_liquidity, final_liquidity, TOLERANCE); + } + + #[test] + #[available_gas(1000000)] + fn test_market_add_liquidity_not_optimal() { + // With initial liquidity + let market = Market { + item_id: 1, cash_amount: SCALING_FACTOR * 1, item_quantity: 10 + }; // pool 1:10 + let initial_liquidity = market.liquidity(); + + // Add liquidity without the same ratio + let (amount, quantity) = (SCALING_FACTOR * 2, 10); // pool 1:5 + + let (amount_add, quantity_add, liquidity_add) = market.add_liquidity(amount, quantity); + + // Assert that the amount added is optimal even though the + // amount originally requested was not + let amount_optimal = SCALING_FACTOR * 1; + assert(amount_add == amount_optimal, 'wrong cash amount'); + assert(quantity_add == quantity, 'wrong item quantity'); + + // Get expected amount and convert to fixed point + let expected_amount = FixedTrait::new_unscaled(SCALING_FACTOR * 1 + amount_add, false); + let expected_quantity: u128 = (10 + quantity).into() * SCALING_FACTOR; + let expected_quantity = FixedTrait::new_unscaled(expected_quantity, false); + + // Get expecteed liquidity + let expected_liquidity = FixedTrait::sqrt(expected_amount * expected_quantity); + + let final_liquidity = initial_liquidity + liquidity_add; + // assert_precise(expected_liquidity, final_liquidity.into(), 'wrong liquidity', Option::None(())); + } + + #[test] + #[should_panic(expected: ('insufficient amount',))] + fn test_market_add_liquidity_insufficient_amount() { + let market = Market { + item_id: 1, cash_amount: SCALING_FACTOR * 1, item_quantity: 10 + }; // pool 1:10 + // Adding 20 items requires (SCALING_FACTOR * 2) cash amount to maintain the ratio + // Therefore this should fail + let (amount_add, quantity_add, liquidity_add) = market + .add_liquidity(SCALING_FACTOR * 1, 20); + } + + #[test] + #[available_gas(1000000)] + fn test_market_remove_liquidity() { + // With initial liquidity + let market = Market { + item_id: 1, cash_amount: SCALING_FACTOR * 2, item_quantity: 20 + }; // pool 1:10 + let initial_liquidity = market.liquidity(); + + // Remove half of the liquidity + let two = FixedTrait::new_unscaled(2, false); + let liquidity_remove = initial_liquidity / two; + + let (amount_remove, quantity_remove) = market.remove_liquidity(liquidity_remove); + + // Assert that the amount and quantity removed are half of the initial amount and quantity + assert(amount_remove == SCALING_FACTOR * 1, 'wrong cash amount'); + assert(quantity_remove == 10, 'wrong item quantity'); + + // Get expected amount and convert to fixed point + let expected_amount = FixedTrait::new_unscaled(SCALING_FACTOR * 2 - amount_remove, false); + let expected_quantity: u128 = (20 - quantity_remove).into() * SCALING_FACTOR; + let expected_quantity = FixedTrait::new_unscaled(expected_quantity, false); + + // Get expecteed liquidity + let expected_liquidity = FixedTrait::sqrt(expected_amount * expected_quantity); + + let final_liquidity = initial_liquidity - liquidity_remove; + // assert_precise(expected_liquidity, final_liquidity.into(), 'wrong liquidity', Option::None(())); + } + + #[test] + #[should_panic(expected: ('insufficient liquidity',))] + fn test_market_remove_liquidity_no_initial() { + // Without initial liquidity + let market = Market { item_id: 1, cash_amount: 0, item_quantity: 0 }; // pool 1:10 + + // Remove liquidity + let one = FixedTrait::new_unscaled(1, false); + + let (amount_remove, quantity_remove) = market.remove_liquidity(one); + } + + #[test] + #[should_panic(expected: ('insufficient liquidity',))] + fn test_market_remove_liquidity_more_than_available() { + // With initial liquidity + let market = Market { + item_id: 1, cash_amount: SCALING_FACTOR * 2, item_quantity: 20 + }; // pool 1:10 + let initial_liquidity = market.liquidity(); + + // Remove twice of the liquidity + let two = FixedTrait::new_unscaled(2, false); + let liquidity_remove = initial_liquidity * two; + + let (amount_remove, quantity_remove) = market.remove_liquidity(liquidity_remove); + } +} diff --git a/examples/market/src/systems/liquidity.cairo b/examples/market/src/systems/liquidity.cairo new file mode 100644 index 00000000..ca6ca349 --- /dev/null +++ b/examples/market/src/systems/liquidity.cairo @@ -0,0 +1,128 @@ +// Dojo imports + +use dojo::world::IWorldDispatcher; + +// Extenal imports + +use cubit::f128::types::fixed::Fixed; + +trait ILiquidity { + fn add( + self: @TContractState, world: IWorldDispatcher, item_id: u32, amount: u128, quantity: u128 + ); + fn remove(self: @TContractState, world: IWorldDispatcher, item_id: u32, shares: Fixed); +} + +#[dojo::contract] +mod Liquidity { + // Internal imports + + use market::models::{ + item::Item, cash::Cash, liquidity::Liquidity, market::{Market, MarketTrait} + }; + + // Local imports + + use super::Fixed; + use super::ILiquidity; + + #[external(v0)] + impl LiquidityImpl of ILiquidity { + fn add( + self: @ContractState, + world: IWorldDispatcher, + item_id: u32, + amount: u128, + quantity: u128 + ) { + let player = starknet::get_caller_address(); + + let item = get!(world, (player, item_id), Item); + let player_quantity = item.quantity; + assert(player_quantity >= quantity, 'not enough items'); + + let player_cash = get!(world, (player), Cash); + assert(amount <= player_cash.amount, 'not enough cash'); + + let market = get!(world, (item_id), Market); + let (cost_cash, cost_quantity, liquidity_shares) = market + .add_liquidity(amount, quantity); + + // update market + set!( + world, + (Market { + item_id: item_id, + cash_amount: market.cash_amount + cost_cash, + item_quantity: market.item_quantity + cost_quantity + }) + ); + + // update player cash + set!(world, (Cash { player: player, amount: player_cash.amount - cost_cash })); + + // update player item + set!( + world, + (Item { + player: player, item_id: item_id, quantity: player_quantity - cost_quantity + }) + ); + + // update player liquidity + let player_liquidity = get!(world, (player, item_id), Liquidity); + set!( + world, + (Liquidity { + player: player, + item_id: item_id, + shares: player_liquidity.shares + liquidity_shares + }) + ); + } + + + fn remove(self: @ContractState, world: IWorldDispatcher, item_id: u32, shares: Fixed) { + let player = starknet::get_caller_address(); + + let player_liquidity = get!(world, (player, item_id), Liquidity); + assert(player_liquidity.shares >= shares, 'not enough shares'); + + let market = get!(world, (item_id), Market); + let (payout_cash, payout_quantity) = market.remove_liquidity(shares); + + // update market + set!( + world, + (Market { + item_id: item_id, + cash_amount: market.cash_amount - payout_cash, + item_quantity: market.item_quantity - payout_quantity + }) + ); + + // update player cash + let player_cash = get!(world, (player), Cash); + set!(world, (Cash { player: player, amount: player_cash.amount + payout_cash })); + + // update player item + let item = get!(world, (player, item_id), Item); + let player_quantity = item.quantity; + set!( + world, + (Item { + player: player, item_id: item_id, quantity: player_quantity + payout_quantity + }) + ); + + // update player liquidity + let player_liquidity = get!(world, (player, item_id), Liquidity); + set!( + world, + (Liquidity { + player: player, item_id: item_id, shares: player_liquidity.shares - shares + }) + ); + } + } +} diff --git a/examples/market/src/systems/trade.cairo b/examples/market/src/systems/trade.cairo new file mode 100644 index 00000000..6bca856b --- /dev/null +++ b/examples/market/src/systems/trade.cairo @@ -0,0 +1,86 @@ +// Dojo imports + +use dojo::world::IWorldDispatcher; + +trait ITrade { + fn buy(self: @TContractState, world: IWorldDispatcher, item_id: u32, quantity: u128); + fn sell(self: @TContractState, world: IWorldDispatcher, item_id: u32, quantity: u128); +} + +#[dojo::contract] +mod Trade { + // Internal imports + + use market::models::{item::Item, cash::Cash, market::{Market, MarketTrait}}; + + // Local imports + + use super::ITrade; + + #[external(v0)] + impl TradeImpl of ITrade { + fn buy(self: @ContractState, world: IWorldDispatcher, item_id: u32, quantity: u128) { + let player = starknet::get_caller_address(); + + let player_cash = get!(world, (player), Cash); + + let market = get!(world, (item_id), Market); + + let cost = market.buy(quantity); + assert(cost <= player_cash.amount, 'not enough cash'); + + // update market + set!( + world, + (Market { + item_id: item_id, + cash_amount: market.cash_amount + cost, + item_quantity: market.item_quantity - quantity, + }) + ); + + // update player cash + set!(world, (Cash { player: player, amount: player_cash.amount - cost })); + + // update player item + let item = get!(world, (player, item_id), Item); + set!( + world, + (Item { player: player, item_id: item_id, quantity: item.quantity + quantity }) + ); + } + + + fn sell(self: @ContractState, world: IWorldDispatcher, item_id: u32, quantity: u128) { + let player = starknet::get_caller_address(); + + let item = get!(world, (player, item_id), Item); + let player_quantity = item.quantity; + assert(player_quantity >= quantity, 'not enough items'); + + let player_cash = get!(world, (player), Cash); + + let market = get!(world, (item_id), Market); + let payout = market.sell(quantity); + + // update market + set!( + world, + (Market { + item_id: item_id, + cash_amount: market.cash_amount - payout, + item_quantity: market.item_quantity + quantity, + }) + ); + + // update player cash + set!(world, (Cash { player: player, amount: player_cash.amount + payout })); + + // update player item + set!( + world, + (Item { player: player, item_id: item_id, quantity: player_quantity - quantity }) + ); + } + } +} diff --git a/examples/market/src/tests/setup.cairo b/examples/market/src/tests/setup.cairo new file mode 100644 index 00000000..be384ca3 --- /dev/null +++ b/examples/market/src/tests/setup.cairo @@ -0,0 +1,41 @@ +// Starknet imports + +use starknet::ContractAddress; + +// Dojo imports + +use dojo::world::{IWorldDispatcherTrait, IWorldDispatcher}; +use dojo::test_utils::{spawn_test_world, deploy_contract}; + +// Internal imports + +use market::models::cash::{cash, Cash}; +use market::models::item::{item, Item}; +use market::models::liquidity::{liquidity, Liquidity}; +use market::models::market::{market as market_model, Market}; +use market::systems::liquidity::{Liquidity as liquidity_actions}; +use market::systems::trade::{Trade as trade_actions}; + +#[derive(Drop)] +struct Systems { + liquidity: ContractAddress, + trade: ContractAddress, +} + +fn spawn_market() -> (IWorldDispatcher, Systems) { + // [Setup] World + let mut models = array::ArrayTrait::new(); + models.append(cash::TEST_CLASS_HASH); + models.append(item::TEST_CLASS_HASH); + models.append(liquidity::TEST_CLASS_HASH); + models.append(market_model::TEST_CLASS_HASH); + let world = spawn_test_world(models); + + // [Setup] Systems + let liquidity_address = deploy_contract(liquidity_actions::TEST_CLASS_HASH, array![].span()); + let trade_address = deploy_contract(trade_actions::TEST_CLASS_HASH, array![].span()); + let systems = Systems { liquidity: liquidity_address, trade: trade_address, }; + + // [Return] + (world, systems) +} diff --git a/examples/market/src/tests/trade.cairo b/examples/market/src/tests/trade.cairo new file mode 100644 index 00000000..873117c5 --- /dev/null +++ b/examples/market/src/tests/trade.cairo @@ -0,0 +1,22 @@ +// Core imports + +use debug::PrintTrait; + +// Dojo imports + +use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait}; + +// Internal imports + +use market::models::cash::Cash; +use market::models::item::Item; +use market::models::liquidity::Liquidity; +use market::models::market::{Market, MarketTrait}; +use market::tests::{setup, setup::Systems}; + +#[test] +#[available_gas(1_000_000_000)] +fn test_market_spawn() { + // [Setup] + let (world, systems) = setup::spawn_market(); +} diff --git a/examples/projectile/Scarb.lock b/examples/projectile/Scarb.lock new file mode 100644 index 00000000..b518dd7b --- /dev/null +++ b/examples/projectile/Scarb.lock @@ -0,0 +1,22 @@ +# Code generated by scarb DO NOT EDIT. +version = 1 + +[[package]] +name = "algebra" +version = "0.0.0" +dependencies = [ + "cubit", +] + +[[package]] +name = "cubit" +version = "1.2.0" +source = "git+https://github.com/influenceth/cubit?rev=b459053#b4590530d5aeae9aabd36740cc2a3d9e6adc5fde" + +[[package]] +name = "projectile" +version = "0.0.0" +dependencies = [ + "algebra", + "cubit", +] diff --git a/examples/projectile/Scarb.toml b/examples/projectile/Scarb.toml new file mode 100644 index 00000000..8b1d87a7 --- /dev/null +++ b/examples/projectile/Scarb.toml @@ -0,0 +1,9 @@ +[package] +name = "projectile" +version = "0.0.0" +description = "Example of algebria crate usage." +homepage = "https://github.com/dojoengine/origami/tree/examples/projectile" + +[dependencies] +cubit = { git = "https://github.com/influenceth/cubit", rev = "b459053" } +algebra = { path = "../../crates/algebra" } \ No newline at end of file diff --git a/examples/projectile/src/lib.cairo b/examples/projectile/src/lib.cairo new file mode 100644 index 00000000..1926d4ef --- /dev/null +++ b/examples/projectile/src/lib.cairo @@ -0,0 +1,257 @@ +// External imports + +use cubit::f128::types::fixed::{Fixed, FixedTrait, ONE_u128}; +use cubit::f128::math::trig; + +// Internal imports + +use algebra::vec2::{Vec2, Vec2Trait}; + +fn main() -> (usize, Array::, Array::) { + // to be inputs for #[view] function + // v_0_mag_felt: felt252, theta_0_deg_felt: felt252, x_0_felt: felt252, y_0_felt: felt252 + + // + // Projectile parameters + // + /// Inputs: to be contract inputs for view function `main` + /// Launch velocity magnitude, 0 <= v_0_felt <= 100 + let v_0_mag_felt = 100; + /// Launch angle in degrees, -180 <= theta_0_deg_felt <= 180 + let theta_0_deg_felt = 65; + /// Initial horizontal position, x_min <= x_0_felt <= x_max + let x_0_felt = 0; + /// Initial vertical position, y_min <= y_0_felt <= y_max + let y_0_felt = 0; + /// Convert inputs to signed fixed-point + let v_0_mag = FixedTrait::from_unscaled_felt(v_0_mag_felt); + let theta_0_deg = FixedTrait::from_unscaled_felt(theta_0_deg_felt); + let x_0 = FixedTrait::from_unscaled_felt(x_0_felt); + let y_0 = FixedTrait::from_unscaled_felt(y_0_felt); + + /// Convert theta_0_deg to radians + let theta_0 = deg_to_rad(theta_0_deg); + + // Gravitational acceleration magnitude + let g = FixedTrait::new(98 * ONE_u128 / 10, false); // 9.8 + // Plot parameters + let x_max = FixedTrait::from_unscaled_felt(1000); + let x_min = FixedTrait::from_unscaled_felt(-1000); + let y_max = FixedTrait::from_unscaled_felt(500); + let y_min = FixedTrait::from_unscaled_felt(-500); + // Check that inputs are within required ranges + assert(v_0_mag.mag <= 100 * ONE_u128, 'need v_0_mag_felt <= 100'); + assert(v_0_mag.mag > 0 * ONE_u128, 'need v_0_mag_felt > 0'); + assert(v_0_mag.sign == false, 'need v_0_mag_felt > 0'); + // `theta_0_deg.mag` not exact after conversion, so use 180.0000001 + assert(theta_0_deg.mag <= 180000001 * ONE_u128 / 1000000, '-180 <= theta_0_deg_felt <= 180'); + assert(x_0 <= x_max, 'need x_0 <= x_max'); + assert(x_0 >= x_min, 'need x_0 >= x_min'); + assert(y_0 <= y_max, 'need y_0 <= y_max'); + assert(y_0 >= y_min, 'need y_0 >= y_min'); + // Initial position vector + let r_0 = Vec2Trait::::new(x_0, y_0); + // Initial velocity vector + let v_0 = vec2_from_mag_theta(v_0_mag, theta_0); + + // Time interval between plotted points + let delta_t = FixedTrait::from_unscaled_felt(2); // arbitrary value 2 chosen + + // Tuples to pass to functions + let plot_params = (x_max, x_min, y_max, y_min); + let motion_params = (r_0, v_0, g, delta_t); + let (mut x_s, mut y_s) = fill_position_s(plot_params, motion_params); + (x_s.len(), x_s, y_s) +} + +fn deg_to_rad(theta_deg: Fixed) -> Fixed { + let pi = FixedTrait::new(trig::PI_u128, false); + let one_eighty = FixedTrait::new(180 * ONE_u128, false); + theta_deg * pi / one_eighty +} + +// Creates Fixed type Vec2 from magnitude, theta in radians +fn vec2_from_mag_theta(mag: Fixed, theta: Fixed) -> Vec2 { + let x_comp = mag * trig::cos(theta); // trig::cos works only for Fixed type + let y_comp = mag * trig::sin(theta); // trig::sin works only for Fixed type + Vec2:: { x: x_comp, y: y_comp } +} + +fn fill_position_s( + plot_params: (Fixed, Fixed, Fixed, Fixed), + motion_params: (Vec2, Vec2, Fixed, Fixed) +) -> (Array::, Array::) { + let (x_max, x_min, _y_max, y_min) = plot_params; + let (r_0, v_0, g, delta_t) = motion_params; + let mut x_s = ArrayTrait::::new(); + let mut y_s = ArrayTrait::::new(); + + let one = FixedTrait::new(ONE_u128, false); + let mut n = FixedTrait::new(0, false); + + loop { + // match withdraw_gas() { + // Option::Some(_) => {}, + // Option::None(_) => { + // let mut data = ArrayTrait::new(); + // data.append('Out of gas'); + // panic(data); + // }, + // } + let t = n * delta_t; + // 'n'.print(); + // n.mag.print(); + let x = calc_x(r_0.x, v_0.x, t); + // 'x'.print(); + // x.mag.print(); + // x.sign.print(); + let y = calc_y(r_0.y, v_0.y, g, t); + // 'y'.print(); + // y.mag.print(); + // y.sign.print(); + if x >= x_max || x <= x_min || y <= y_min { + break (); + } + + x_s.append(x); + y_s.append(y); + + n += one; + }; + + (x_s, y_s) +} + +fn calc_x(x_0: Fixed, v_0x: Fixed, t: Fixed) -> Fixed { + x_0 + v_0x * t +} + +fn calc_y(y_0: Fixed, v_0y: Fixed, g: Fixed, t: Fixed) -> Fixed { + let half = FixedTrait::new(5 * ONE_u128 / 10, false); + y_0 + v_0y * t - half * g * t * t +} + +#[cfg(test)] +mod tests { + // External imports + + use cubit::f128::test::helpers::assert_precise; + + // Local imports + + use super::{deg_to_rad, fill_position_s, vec2_from_mag_theta, calc_x, calc_y}; + use super::{Vec2Trait, Fixed, FixedTrait, ONE_u128}; + #[test] + #[available_gas(2000000)] + fn test_deg_to_rad() { + let sixty = FixedTrait::new(60 * ONE_u128, false); + let theta = deg_to_rad(sixty); + assert_precise(theta, 19317385221538994246, 'invalid PI/3', Option::None(())); + assert(theta.sign == false, 'invalid sign'); + + let minus_120 = FixedTrait::new(120 * ONE_u128, true); + let theta = deg_to_rad(minus_120); + assert_precise(theta, -38634770443077988493, 'invalid -2*PI/3', Option::None(())); + assert(theta.sign == true, 'invalid sign'); + } + + #[test] + #[available_gas(20000000)] + fn test_vec2_from_mag_theta() { + let mag = FixedTrait::new(100 * ONE_u128, false); + let sixty = FixedTrait::new(60 * ONE_u128, false); + let theta = deg_to_rad(sixty); + let vec2 = vec2_from_mag_theta(mag, theta); + assert_precise(vec2.x, 922337203685477580800, 'invalid vec2.x mag', Option::None(())); // 50 + assert(vec2.x.sign == false, 'invalid vec2.x.sign'); + assert_precise( + vec2.y, 1597534898494251510150, 'invalid vec2.y mag', Option::None(()) + ); // 86.6 + assert(vec2.y.sign == false, 'invalid vec2.y.sig'); + + let minus_120 = FixedTrait::new(120 * ONE_u128, true); + let theta = deg_to_rad(minus_120); + let vec2 = vec2_from_mag_theta(mag, theta); + assert_precise( + vec2.x, -922337203685477580800, 'invalid vec2.x mag', Option::None(()) + ); // -50 + assert(vec2.x.sign == true, 'invalid vec2.x.sign'); + assert_precise( + vec2.y, -1597534898494251510150, 'invalid vec2.y mag', Option::None(()) + ); // -86.6 + assert(vec2.y.sign == true, 'invalid vec2.y.sig'); + } + + #[test] + #[available_gas(20000000)] + fn test_fill_position_s() { + let v_0_mag = FixedTrait::from_unscaled_felt(100); + let theta_0_deg = FixedTrait::from_unscaled_felt(65); + let theta_0 = deg_to_rad(theta_0_deg); + let x_0 = FixedTrait::from_unscaled_felt(0); + let y_0 = FixedTrait::from_unscaled_felt(0); + + let x_max = FixedTrait::from_unscaled_felt(1000); + let x_min = FixedTrait::from_unscaled_felt(-1000); + let y_max = FixedTrait::from_unscaled_felt(500); + let y_min = FixedTrait::from_unscaled_felt(-500); + + let r_0 = Vec2Trait::::new(x_0, y_0); + let v_0 = vec2_from_mag_theta(v_0_mag, theta_0); + let g = FixedTrait::new(98 * ONE_u128 / 10, false); + let delta_t = FixedTrait::from_unscaled_felt(2); + + let plot_params = (x_max, x_min, y_max, y_min); + let motion_params = (r_0, v_0, g, delta_t); + + let mut position_s: (Array, Array) = fill_position_s( + plot_params, motion_params + ); + + let (x_s, y_s) = position_s; + let length = x_s.len(); + assert(length == 12, 'invalid length'); + + assert_precise( + *x_s[5], 7795930915206679528264, 'invalid x_s[5]', Option::None(()) + ); // 422.61826174069944 + assert(*x_s.at(5).sign == false, 'invalid sign'); + assert_precise( + *y_s[5], 7679523203357457794972, 'invalid y_s[5]', Option::None(()) + ); // 416.3077870366498 + assert(*y_s.at(5).sign == false, 'invalid sign'); + + assert_precise( + *x_s[10], 15591861830413359425462, 'invalid x_s[10]', Option::None(()) + ); // 845.2365234813989, custom precision 1e-6 + assert(*x_s.at(10).sign == false, 'invalid sign'); + assert_precise( + *y_s[10], -2718762785520446838411, 'invalid y_s[10]', Option::None(()) + ); // -147.3844259267005, custom precision 1e-6 + assert(*y_s.at(10).sign == true, 'invalid sign'); + } + + #[test] + #[available_gas(2000000)] + fn test_calc_x() { + let x_0 = FixedTrait::new(100 * ONE_u128, false); + let v_0x = FixedTrait::new(50 * ONE_u128, false); + let t = FixedTrait::new(16 * ONE_u128, false); + let x = calc_x(x_0, v_0x, t); + assert(x.mag == 900 * ONE_u128, 'invalid mag'); + assert(x.sign == false, 'invalid sign'); + } + + #[test] + #[available_gas(2000000)] + fn test_calc_y() { + let y_0 = FixedTrait::new(100 * ONE_u128, false); + let v_0y = FixedTrait::new(50 * ONE_u128, false); + let t = FixedTrait::new(16 * ONE_u128, false); + let g = FixedTrait::new(98 * ONE_u128 / 10, false); + + let y = calc_y(y_0, v_0y, g, t); + assert_precise(y, -6537526099722665092710, 'invalid y', Option::None(())); // -354.4 + assert(y.sign == true, 'invalid sign'); + } +} diff --git a/presets/.gitignore b/presets/.gitignore new file mode 100644 index 00000000..1de56593 --- /dev/null +++ b/presets/.gitignore @@ -0,0 +1 @@ +target \ No newline at end of file diff --git a/presets/Scarb.lock b/presets/Scarb.lock new file mode 100644 index 00000000..3a767aec --- /dev/null +++ b/presets/Scarb.lock @@ -0,0 +1,27 @@ +# Code generated by scarb DO NOT EDIT. +version = 1 + +[[package]] +name = "dojo" +version = "0.3.4" +source = "git+https://github.com/dojoengine/dojo.git?rev=d62ec8a#d62ec8a522f92d0a0ccf226c6e929bb918b31128" +dependencies = [ + "dojo_plugin", +] + +[[package]] +name = "dojo_plugin" +version = "0.3.4" + +[[package]] +name = "openzeppelin" +version = "0.8.0-beta.0" +source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?rev=4eafccf#4eafccf107f98d975b20cbb6544b2a998e5f403f" + +[[package]] +name = "presets" +version = "0.0.0" +dependencies = [ + "dojo", + "openzeppelin", +] diff --git a/presets/Scarb.toml b/presets/Scarb.toml new file mode 100644 index 00000000..67dbbe95 --- /dev/null +++ b/presets/Scarb.toml @@ -0,0 +1,9 @@ +[package] +name = "presets" +version = "0.0.0" +description = "Implementations of ERC standards for the Dojo framework." +homepage = "https://github.com/dojoengine/origami/tree/presets" + +[dependencies] +dojo = { git = "https://github.com/dojoengine/dojo.git", rev = "d62ec8a" } +openzeppelin = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", rev = "4eafccf" } \ No newline at end of file diff --git a/presets/src/erc1155/erc1155.cairo b/presets/src/erc1155/erc1155.cairo new file mode 100644 index 00000000..af50183e --- /dev/null +++ b/presets/src/erc1155/erc1155.cairo @@ -0,0 +1,5 @@ +mod erc1155; +mod models; +mod interface; + +use erc1155::ERC1155; \ No newline at end of file diff --git a/presets/src/erc1155/erc1155/erc1155.cairo b/presets/src/erc1155/erc1155/erc1155.cairo new file mode 100644 index 00000000..26ba7f23 --- /dev/null +++ b/presets/src/erc1155/erc1155/erc1155.cairo @@ -0,0 +1,439 @@ +#[starknet::contract] +mod ERC1155 { + use dojo_erc::token::erc1155::models::{ + ERC1155Meta, ERC1155OperatorApproval, ERC1155Balance + }; + use dojo_erc::token::erc1155::interface; + use dojo_erc::token::erc1155::interface::{IERC1155, IERC1155CamelOnly}; + use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait}; + use starknet::ContractAddress; + use starknet::{get_caller_address, get_contract_address}; + use array::ArrayTCloneImpl; + use zeroable::Zeroable; + use debug::PrintTrait; + + #[storage] + struct Storage { + _world: ContractAddress, + } + + #[event] + #[derive(Clone, Drop, starknet::Event)] + enum Event { + TransferSingle: TransferSingle, + TransferBatch: TransferBatch, + ApprovalForAll: ApprovalForAll + } + + #[derive(Clone, Drop, starknet::Event)] + struct TransferSingle { + operator: ContractAddress, + from: ContractAddress, + to: ContractAddress, + id: u256, + value: u256 + } + + #[derive(Clone, Drop, starknet::Event)] + struct TransferBatch { + operator: ContractAddress, + from: ContractAddress, + to: ContractAddress, + ids: Array, + values: Array + } + + #[derive(Clone, Drop, starknet::Event)] + struct ApprovalForAll { + owner: ContractAddress, + operator: ContractAddress, + approved: bool + } + + mod Errors { + const INVALID_TOKEN_ID: felt252 = 'ERC1155: invalid token ID'; + const INVALID_ACCOUNT: felt252 = 'ERC1155: invalid account'; + const UNAUTHORIZED: felt252 = 'ERC1155: unauthorized caller'; + const SELF_APPROVAL: felt252 = 'ERC1155: self approval'; + const INVALID_RECEIVER: felt252 = 'ERC1155: invalid receiver'; + const WRONG_SENDER: felt252 = 'ERC1155: wrong sender'; + const SAFE_MINT_FAILED: felt252 = 'ERC1155: safe mint failed'; + const SAFE_TRANSFER_FAILED: felt252 = 'ERC1155: safe transfer failed'; + const INVALID_ARRAY_LENGTH: felt252 = 'ERC1155: invalid array length'; + const INSUFFICIENT_BALANCE: felt252 = 'ERC1155: insufficient balance'; + } + + #[constructor] + fn constructor( + ref self: ContractState, + world: ContractAddress, + name: felt252, + symbol: felt252, + base_uri: felt252, + ) { + self._world.write(world); + self.initializer(name, symbol, base_uri); + } + + // + // External + // + + // #[external(v0)] + // impl SRC5Impl of ISRC5 { + // fn supports_interface(self: @ContractState, interface_id: felt252) -> bool { + // let unsafe_state = src5::SRC5::unsafe_new_contract_state(); + // src5::SRC5::SRC5Impl::supports_interface(@unsafe_state, interface_id) + // } + // } + + // #[external(v0)] + // impl SRC5CamelImpl of ISRC5Camel { + // fn supportsInterface(self: @ContractState, interfaceId: felt252) -> bool { + // let unsafe_state = src5::SRC5::unsafe_new_contract_state(); + // src5::SRC5::SRC5CamelImpl::supportsInterface(@unsafe_state, interfaceId) + // } + // } + + #[external(v0)] + impl ERC1155MetadataImpl of interface::IERC1155Metadata { + fn name(self: @ContractState) -> felt252 { + self.get_meta().name + } + + fn symbol(self: @ContractState) -> felt252 { + self.get_meta().symbol + } + + fn uri(self: @ContractState, token_id: u256) -> felt252 { + //assert(self._exists(token_id), Errors::INVALID_TOKEN_ID); + // TODO : concat with id + self.get_uri(token_id) + } + } + + + #[external(v0)] + impl ERC1155Impl of interface::IERC1155 { + fn balance_of(self: @ContractState, account: ContractAddress, id: u256) -> u256 { + assert(account.is_non_zero(), Errors::INVALID_ACCOUNT); + self.get_balance(account, id).amount + } + + fn balance_of_batch( + self: @ContractState, accounts: Array, ids: Array + ) -> Array { + assert(ids.len() == accounts.len(), Errors::INVALID_ARRAY_LENGTH); + + let mut batch_balances = array![]; + let mut index = 0; + loop { + if index == ids.len() { + break batch_balances.clone(); + } + batch_balances.append(self.balance_of(*accounts.at(index), *ids.at(index))); + index += 1; + } + } + + fn set_approval_for_all( + ref self: ContractState, operator: ContractAddress, approved: bool + ) { + self._set_approval_for_all(get_caller_address(), operator, approved) + } + + fn is_approved_for_all( + self: @ContractState, account: ContractAddress, operator: ContractAddress + ) -> bool { + self.get_operator_approval(account, operator).approved + } + + fn safe_transfer_from( + ref self: ContractState, + from: ContractAddress, + to: ContractAddress, + id: u256, + amount: u256, + data: Array + ) { + assert(to.is_non_zero(), Errors::INVALID_RECEIVER); + assert(from.is_non_zero(), Errors::WRONG_SENDER); + assert( + self._is_approved_for_all_or_owner(from, get_caller_address()), Errors::UNAUTHORIZED + ); + + self._safe_transfer_from(from, to, id, amount, data); + } + + fn safe_batch_transfer_from( + ref self: ContractState, + from: ContractAddress, + to: ContractAddress, + ids: Array, + amounts: Array, + data: Array + ) { + assert(to.is_non_zero(), Errors::INVALID_RECEIVER); + assert(from.is_non_zero(), Errors::WRONG_SENDER); + assert( + self._is_approved_for_all_or_owner(from, get_caller_address()), Errors::UNAUTHORIZED + ); + + self._safe_batch_transfer_from(from, to, ids, amounts, data); + } + } + + #[external(v0)] + impl ERC1155CamelOnlyImpl of interface::IERC1155CamelOnly { + fn balanceOf(self: @ContractState, account: ContractAddress, id: u256) -> u256 { + ERC1155Impl::balance_of(self, account, id) + } + + fn balanceOfBatch( + self: @ContractState, accounts: Array, ids: Array + ) -> Array { + ERC1155Impl::balance_of_batch(self, accounts, ids) + } + + fn setApprovalForAll(ref self: ContractState, operator: ContractAddress, approved: bool) { + ERC1155Impl::set_approval_for_all(ref self, operator, approved); + } + fn isApprovedForAll( + self: @ContractState, account: ContractAddress, operator: ContractAddress + ) -> bool { + ERC1155Impl::is_approved_for_all(self, account, operator) + } + fn safeTransferFrom( + ref self: ContractState, + from: ContractAddress, + to: ContractAddress, + id: u256, + amount: u256, + data: Array + ) { + ERC1155Impl::safe_transfer_from(ref self, from, to, id, amount, data); + } + fn safeBatchTransferFrom( + ref self: ContractState, + from: ContractAddress, + to: ContractAddress, + ids: Array, + amounts: Array, + data: Array + ) { + ERC1155Impl::safe_batch_transfer_from(ref self, from, to, ids, amounts, data); + } + } + + // + // Internal + // + + #[generate_trait] + impl WorldInteractionsImpl of WorldInteractionsTrait { + fn world(self: @ContractState) -> IWorldDispatcher { + IWorldDispatcher { contract_address: self._world.read() } + } + + fn get_meta(self: @ContractState) -> ERC1155Meta { + get!(self.world(), get_contract_address(), ERC1155Meta) + } + + fn get_uri(self: @ContractState, token_id: u256) -> felt252 { + // TODO : concat with id when we have string type + self.get_meta().base_uri + } + + fn get_balance(self: @ContractState, account: ContractAddress, id: u256) -> ERC1155Balance { + get!(self.world(), (get_contract_address(), account, id), ERC1155Balance) + } + + fn get_operator_approval( + self: @ContractState, owner: ContractAddress, operator: ContractAddress + ) -> ERC1155OperatorApproval { + get!(self.world(), (get_contract_address(), owner, operator), ERC1155OperatorApproval) + } + + fn set_operator_approval( + ref self: ContractState, + owner: ContractAddress, + operator: ContractAddress, + approved: bool + ) { + set!( + self.world(), + ERC1155OperatorApproval { token: get_contract_address(), owner, operator, approved } + ); + self.emit_event(ApprovalForAll { owner, operator, approved }); + } + + fn set_balance(ref self: ContractState, account: ContractAddress, id: u256, amount: u256) { + set!( + self.world(), ERC1155Balance { token: get_contract_address(), account, id, amount } + ); + } + + fn update_balances( + ref self: ContractState, + from: ContractAddress, + to: ContractAddress, + id: u256, + amount: u256, + ) { + self.set_balance(from, id, self.get_balance(from, id).amount - amount); + self.set_balance(to, id, self.get_balance(to, id).amount + amount); + } + + fn emit_event< + S, impl IntoImp: traits::Into, impl SDrop: Drop, impl SClone: Clone + >( + ref self: ContractState, event: S + ) { + self.emit(event.clone()); + emit!(self.world(), event); + } + } + + #[generate_trait] + impl InternalImpl of InternalTrait { + fn initializer(ref self: ContractState, name: felt252, symbol: felt252, base_uri: felt252) { + let meta = ERC1155Meta { token: get_contract_address(), name, symbol, base_uri }; + set!(self.world(), (meta)); + // let mut unsafe_state = src5::SRC5::unsafe_new_contract_state(); + // src5::SRC5::InternalImpl::register_interface(ref unsafe_state, interface::IERC721_ID); + // src5::SRC5::InternalImpl::register_interface( + // ref unsafe_state, interface::IERC721_METADATA_ID + // ); + } + + fn _is_approved_for_all_or_owner( + self: @ContractState, from: ContractAddress, caller: ContractAddress + ) -> bool { + caller == from || self.is_approved_for_all(from, caller) + } + + fn _set_approval_for_all( + ref self: ContractState, + owner: ContractAddress, + operator: ContractAddress, + approved: bool + ) { + assert(owner != operator, Errors::SELF_APPROVAL); + self.set_operator_approval(owner, operator, approved); + } + + fn _safe_transfer_from( + ref self: ContractState, + from: ContractAddress, + to: ContractAddress, + id: u256, + amount: u256, + data: Array + ) { + self.update_balances(from, to, id, amount); + // assert( + // _check_on_erc1155_received(from, to, id, data), Errors::SAFE_TRANSFER_FAILED + // ); + + self + .emit_event( + TransferSingle { operator: get_caller_address(), from, to, id, value: amount } + ); + } + + fn _safe_batch_transfer_from( + ref self: ContractState, + from: ContractAddress, + to: ContractAddress, + ids: Array, + amounts: Array, + data: Array + ) { + assert(ids.len() == amounts.len(), Errors::INVALID_ARRAY_LENGTH); + + let mut ids_span = ids.span(); + let mut amounts_span = amounts.span(); + + loop { + if ids_span.len() == 0 { + break (); + } + let id = *ids_span.pop_front().unwrap(); + let amount = *amounts_span.pop_front().unwrap(); + self.update_balances(from, to, id, amount); + // assert( + // _check_on_erc1155_received(from, to, id, data), Errors::SAFE_TRANSFER_FAILED + // ); + }; + + self + .emit_event( + TransferBatch { operator: get_caller_address(), from, to, ids, values: amounts } + ); + } + + fn _mint(ref self: ContractState, to: ContractAddress, id: u256, amount: u256) { + assert(to.is_non_zero(), Errors::INVALID_RECEIVER); + + self.set_balance(to, id, self.get_balance(to, id).amount + amount); + + self + .emit_event( + TransferSingle { + operator: get_caller_address(), + from: Zeroable::zero(), + to, + id, + value: amount + } + ); + } + + fn _burn(ref self: ContractState, id: u256, amount: u256) { + let caller = get_caller_address(); + assert(self.get_balance(caller, id).amount >= amount, Errors::INSUFFICIENT_BALANCE); + + self.set_balance(caller, id, self.get_balance(caller, id).amount - amount); + + self + .emit_event( + TransferSingle { + operator: get_caller_address(), + from: caller, + to: Zeroable::zero(), + id, + value: amount + } + ); + } + + fn _safe_mint( + ref self: ContractState, + to: ContractAddress, + id: u256, + amount: u256, + data: Span + ) { + self._mint(to, id, amount); + // assert( + // _check_on_erc1155_received(Zeroable::zero(), to, id, data), + // Errors::SAFE_MINT_FAILED + // ); + } + } +//#[internal] +// fn _check_on_erc1155_received( +// from: ContractAddress, to: ContractAddress, token_id: u256, data: Span +// ) -> bool { +// if (DualCaseSRC5 { contract_address: to } +// .supports_interface(interface::IERC1155_RECEIVER_ID)) { +// DualCaseERC1155Receiver { contract_address: to } +// .on_erc1155_received( +// get_caller_address(), from, token_id, data +// ) == interface::IERC1155_RECEIVER_ID +// } else { +// DualCaseSRC5 { contract_address: to }.supports_interface(account::interface::ISRC6_ID) +// } +// } + +} diff --git a/presets/src/erc1155/erc1155/interface.cairo b/presets/src/erc1155/erc1155/interface.cairo new file mode 100644 index 00000000..ecce8fb9 --- /dev/null +++ b/presets/src/erc1155/erc1155/interface.cairo @@ -0,0 +1,62 @@ +use starknet::ContractAddress; + +#[starknet::interface] +trait IERC1155 { + fn balance_of(self: @TState, account: ContractAddress, id: u256) -> u256; + fn balance_of_batch( + self: @TState, accounts: Array, ids: Array + ) -> Array; + fn set_approval_for_all(ref self: TState, operator: ContractAddress, approved: bool); + fn is_approved_for_all( + self: @TState, account: ContractAddress, operator: ContractAddress + ) -> bool; + fn safe_transfer_from( + ref self: TState, + from: ContractAddress, + to: ContractAddress, + id: u256, + amount: u256, + data: Array + ); + fn safe_batch_transfer_from( + ref self: TState, + from: ContractAddress, + to: ContractAddress, + ids: Array, + amounts: Array, + data: Array + ); +} + +#[starknet::interface] +trait IERC1155CamelOnly { + fn balanceOf(self: @TState, account: ContractAddress, id: u256) -> u256; + fn balanceOfBatch( + self: @TState, accounts: Array, ids: Array + ) -> Array; + fn setApprovalForAll(ref self: TState, operator: ContractAddress, approved: bool); + fn isApprovedForAll(self: @TState, account: ContractAddress, operator: ContractAddress) -> bool; + fn safeTransferFrom( + ref self: TState, + from: ContractAddress, + to: ContractAddress, + id: u256, + amount: u256, + data: Array + ); + fn safeBatchTransferFrom( + ref self: TState, + from: ContractAddress, + to: ContractAddress, + ids: Array, + amounts: Array, + data: Array + ); +} + +#[starknet::interface] +trait IERC1155Metadata { + fn name(self: @TState) -> felt252; + fn symbol(self: @TState) -> felt252; + fn uri(self: @TState, token_id: u256) -> felt252; +} diff --git a/presets/src/erc1155/erc1155/models.cairo b/presets/src/erc1155/erc1155/models.cairo new file mode 100644 index 00000000..fa021aa9 --- /dev/null +++ b/presets/src/erc1155/erc1155/models.cairo @@ -0,0 +1,33 @@ +use starknet::ContractAddress; + +#[derive(Model, Copy, Drop, Serde)] +struct ERC1155Meta { + #[key] + token: ContractAddress, + name: felt252, + symbol: felt252, + base_uri: felt252, +} + +#[derive(Model, Copy, Drop, Serde)] +struct ERC1155OperatorApproval { + #[key] + token: ContractAddress, + #[key] + owner: ContractAddress, + #[key] + operator: ContractAddress, + approved: bool +} + + +#[derive(Model, Copy, Drop, Serde)] +struct ERC1155Balance { + #[key] + token: ContractAddress, + #[key] + account: ContractAddress, + #[key] + id: u256, + amount: u256 +} \ No newline at end of file diff --git a/presets/src/erc1155/tests.cairo b/presets/src/erc1155/tests.cairo new file mode 100644 index 00000000..6951695b --- /dev/null +++ b/presets/src/erc1155/tests.cairo @@ -0,0 +1,836 @@ +use dojo_erc::tests::utils; +use dojo_erc::tests::constants::{ + ZERO, OWNER, SPENDER, RECIPIENT, OPERATOR, OTHER, NAME, SYMBOL, URI, TOKEN_ID, TOKEN_AMOUNT, + TOKEN_ID_2, TOKEN_AMOUNT_2 +}; + +use dojo_erc::token::erc1155::ERC1155::ERC1155Impl; +use dojo_erc::token::erc1155::ERC1155::ERC1155CamelOnlyImpl; +use dojo_erc::token::erc1155::ERC1155::ERC1155MetadataImpl; +use dojo_erc::token::erc1155::ERC1155::InternalImpl; +use dojo_erc::token::erc1155::ERC1155::WorldInteractionsImpl; +use dojo_erc::token::erc1155::ERC1155::{TransferSingle, TransferBatch, ApprovalForAll}; +use dojo_erc::token::erc1155::ERC1155; +use starknet::ContractAddress; +use starknet::contract_address_const; +use starknet::testing; +use zeroable::Zeroable; +use dojo::test_utils::spawn_test_world; +use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait}; + +use dojo_erc::token::erc1155::models::{ + ERC1155Meta, erc_1155_meta, ERC1155OperatorApproval, erc_1155_operator_approval, ERC1155Balance, + erc_1155_balance +}; +use dojo_erc::token::erc1155::ERC1155::_worldContractMemberStateTrait; +use debug::PrintTrait; + +// +// Setup +// + +fn STATE() -> (IWorldDispatcher, ERC1155::ContractState) { + let world = spawn_test_world( + array![ + erc_1155_meta::TEST_CLASS_HASH, + erc_1155_operator_approval::TEST_CLASS_HASH, + erc_1155_balance::TEST_CLASS_HASH, + ] + ); + let mut state = ERC1155::contract_state_for_testing(); + state._world.write(world.contract_address); + + InternalImpl::_mint(ref state, OWNER(), TOKEN_ID, TOKEN_AMOUNT); + utils::drop_event(ZERO()); + + InternalImpl::_mint(ref state, OWNER(), TOKEN_ID_2, TOKEN_AMOUNT_2); + utils::drop_event(ZERO()); + + (world, state) +} + +fn setup() -> ERC1155::ContractState { + let (world, mut state) = STATE(); + ERC1155::constructor(ref state, world.contract_address, NAME, SYMBOL, URI); + utils::drop_event(ZERO()); + state +} + +// +// initializer & constructor +// + +#[test] +#[available_gas(20000000)] +fn test_constructor() { + let (world, mut state) = STATE(); + ERC1155::constructor(ref state, world.contract_address, NAME, SYMBOL, URI); + + assert(ERC1155MetadataImpl::name(@state) == NAME, 'Name should be NAME'); + assert(ERC1155MetadataImpl::symbol(@state) == SYMBOL, 'Symbol should be SYMBOL'); + assert(ERC1155MetadataImpl::uri(@state, 0) == URI, 'Uri should be URI'); +// assert( +// SRC5Impl::supports_interface(@state, erc1155::interface::IERC1155_ID), 'Missing interface ID' +// ); +// assert( +// SRC5Impl::supports_interface(@state, erc1155::interface::IERC1155_METADATA_ID), +// 'missing interface ID' +// ); +// assert( +// SRC5Impl::supports_interface(@state, introspection::interface::ISRC5_ID), +// 'missing interface ID' +// ); +} + +#[test] +#[available_gas(20000000)] +fn test_initializer() { + let (world, mut state) = STATE(); + InternalImpl::initializer(ref state, NAME, SYMBOL, URI); + + assert(ERC1155MetadataImpl::name(@state) == NAME, 'Name should be NAME'); + assert(ERC1155MetadataImpl::symbol(@state) == SYMBOL, 'Symbol should be SYMBOL'); + + assert(ERC1155Impl::balance_of(@state, OWNER(), 0) == 0, 'Balance should be zero'); + assert( + ERC1155Impl::balance_of(@state, OWNER(), TOKEN_ID) == TOKEN_AMOUNT, 'should be TOKEN_AMOUNT' + ); + assert( + ERC1155Impl::balance_of(@state, OWNER(), TOKEN_ID_2) == TOKEN_AMOUNT_2, + 'should be TOKEN_AMOUNT_2' + ); +} + + +// +// Getters +// + +#[test] +#[available_gas(20000000)] +fn test_balance_of() { + let mut state = setup(); + + assert( + ERC1155Impl::balance_of(@state, OWNER(), TOKEN_ID) == TOKEN_AMOUNT, 'Should return balance' + ); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC1155: invalid account',))] +fn test_balance_of_zero() { + let state = setup(); + ERC1155Impl::balance_of(@state, ZERO(), TOKEN_ID); +} + + +#[test] +#[available_gas(20000000)] +fn test_balance_of_batch() { + let mut state = setup(); + + InternalImpl::_mint(ref state, OTHER(), TOKEN_ID_2, TOKEN_AMOUNT_2); + + let balances = ERC1155Impl::balance_of_batch( + @state, array![OWNER(), OTHER()], array![TOKEN_ID, TOKEN_ID_2] + ); + + assert(*balances.at(0) == TOKEN_AMOUNT, 'Should return TOKEN_AMOUNT'); + assert(*balances.at(1) == TOKEN_AMOUNT_2, 'Should return TOKEN_AMOUNT_2'); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC1155: invalid account',))] +fn test_balance_of_batch_zero() { + let state = setup(); + ERC1155Impl::balance_of_batch(@state, array![OTHER(), ZERO()], array![TOKEN_ID_2, TOKEN_ID]); +} + + +// +// set_approval_for_all & _set_approval_for_all +// + +#[test] +#[available_gas(20000000)] +fn test_set_approval_for_all() { + let (world, mut state) = STATE(); + testing::set_caller_address(OWNER()); + + assert(!ERC1155Impl::is_approved_for_all(@state, OWNER(), OPERATOR()), 'Invalid default value'); + + ERC1155Impl::set_approval_for_all(ref state, OPERATOR(), true); + assert_event_approval_for_all(OWNER(), OPERATOR(), true); + + assert( + ERC1155Impl::is_approved_for_all(@state, OWNER(), OPERATOR()), + 'Operator not approved correctly' + ); + + ERC1155Impl::set_approval_for_all(ref state, OPERATOR(), false); + assert_event_approval_for_all(OWNER(), OPERATOR(), false); + + assert( + !ERC1155Impl::is_approved_for_all(@state, OWNER(), OPERATOR()), + 'Approval not revoked correctly' + ); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC1155: self approval',))] +fn test_set_approval_for_all_owner_equal_operator_true() { + let (world, mut state) = STATE(); + testing::set_caller_address(OWNER()); + ERC1155Impl::set_approval_for_all(ref state, OWNER(), true); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC1155: self approval',))] +fn test_set_approval_for_all_owner_equal_operator_false() { + let (world, mut state) = STATE(); + testing::set_caller_address(OWNER()); + ERC1155Impl::set_approval_for_all(ref state, OWNER(), false); +} + +#[test] +#[available_gas(20000000)] +fn test__set_approval_for_all() { + let (world, mut state) = STATE(); + assert(!ERC1155Impl::is_approved_for_all(@state, OWNER(), OPERATOR()), 'Invalid default value'); + + InternalImpl::_set_approval_for_all(ref state, OWNER(), OPERATOR(), true); + assert_event_approval_for_all(OWNER(), OPERATOR(), true); + + assert( + ERC1155Impl::is_approved_for_all(@state, OWNER(), OPERATOR()), + 'Operator not approved correctly' + ); + + InternalImpl::_set_approval_for_all(ref state, OWNER(), OPERATOR(), false); + assert_event_approval_for_all(OWNER(), OPERATOR(), false); + + assert( + !ERC1155Impl::is_approved_for_all(@state, OWNER(), OPERATOR()), + 'Operator not approved correctly' + ); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC1155: self approval',))] +fn test__set_approval_for_all_owner_equal_operator_true() { + let (world, mut state) = STATE(); + InternalImpl::_set_approval_for_all(ref state, OWNER(), OWNER(), true); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC1155: self approval',))] +fn test__set_approval_for_all_owner_equal_operator_false() { + let (world, mut state) = STATE(); + InternalImpl::_set_approval_for_all(ref state, OWNER(), OWNER(), false); +} + + +// +// safe_transfer_from & safeTransferFrom +// + +#[test] +#[available_gas(50000000)] +fn test_safe_transfer_from_owner() { + let mut state = setup(); + let id = TOKEN_ID; + let amount = TOKEN_AMOUNT; + let owner = OWNER(); + let recipient = RECIPIENT(); + + assert_state_before_transfer(@state, owner, recipient, id); + + testing::set_caller_address(owner); + ERC1155Impl::safe_transfer_from(ref state, owner, recipient, id, amount, array![]); + assert_event_transfer_single(owner, recipient, id, amount); + + assert_state_after_transfer(@state, owner, recipient, id); +} + +#[test] +#[available_gas(50000000)] +fn test_transferFrom_owner() { + let mut state = setup(); + let id = TOKEN_ID; + let amount = TOKEN_AMOUNT; + let owner = OWNER(); + let recipient = RECIPIENT(); + + assert_state_before_transfer(@state, owner, recipient, id); + + testing::set_caller_address(owner); + ERC1155CamelOnlyImpl::safeTransferFrom(ref state, owner, recipient, id, amount, array![]); + assert_event_transfer_single(owner, recipient, id, amount); + + assert_state_after_transfer(@state, owner, recipient, id); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC1155: wrong sender',))] +fn test_safe_transfer_from_zero() { + let (world, mut state) = STATE(); + ERC1155Impl::safe_transfer_from( + ref state, ZERO(), RECIPIENT(), TOKEN_ID, TOKEN_AMOUNT, array![] + ); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC1155: wrong sender',))] +fn test_safeTransferFrom_zero() { + let (world, mut state) = STATE(); + ERC1155CamelOnlyImpl::safeTransferFrom( + ref state, ZERO(), RECIPIENT(), TOKEN_ID, TOKEN_AMOUNT, array![] + ); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC1155: invalid receiver',))] +fn test_safe_transfer_from_to_zero() { + let mut state = setup(); + testing::set_caller_address(OWNER()); + ERC1155Impl::safe_transfer_from(ref state, OWNER(), ZERO(), TOKEN_ID, TOKEN_AMOUNT, array![]); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC1155: invalid receiver',))] +fn test_safeTransferFrom_to_zero() { + let mut state = setup(); + testing::set_caller_address(OWNER()); + ERC1155CamelOnlyImpl::safeTransferFrom( + ref state, OWNER(), ZERO(), TOKEN_ID, TOKEN_AMOUNT, array![] + ); +} + +#[test] +#[available_gas(50000000)] +fn test_safe_transfer_from_to_owner() { + let mut state = setup(); + + assert( + ERC1155Impl::balance_of(@state, OWNER(), TOKEN_ID) == TOKEN_AMOUNT, + 'Balance of owner before' + ); + + testing::set_caller_address(OWNER()); + ERC1155Impl::safe_transfer_from(ref state, OWNER(), OWNER(), TOKEN_ID, TOKEN_AMOUNT, array![]); + assert_event_transfer_single(OWNER(), OWNER(), TOKEN_ID, TOKEN_AMOUNT); + + assert( + ERC1155Impl::balance_of(@state, OWNER(), TOKEN_ID) == TOKEN_AMOUNT, 'Balance of owner after' + ); +} + +#[test] +#[available_gas(50000000)] +fn test_safeTransferFrom_to_owner() { + let mut state = setup(); + + assert( + ERC1155Impl::balance_of(@state, OWNER(), TOKEN_ID) == TOKEN_AMOUNT, + 'Balance of owner before' + ); + + testing::set_caller_address(OWNER()); + ERC1155CamelOnlyImpl::safeTransferFrom( + ref state, OWNER(), OWNER(), TOKEN_ID, TOKEN_AMOUNT, array![] + ); + assert_event_transfer_single(OWNER(), OWNER(), TOKEN_ID, TOKEN_AMOUNT); + + assert( + ERC1155Impl::balance_of(@state, OWNER(), TOKEN_ID) == TOKEN_AMOUNT, 'Balance of owner after' + ); +} + +#[test] +#[available_gas(50000000)] +fn test_transfer_from_approved_for_all() { + let mut state = setup(); + let id = TOKEN_ID; + let amount = TOKEN_AMOUNT; + let owner = OWNER(); + let recipient = RECIPIENT(); + + assert_state_before_transfer(@state, owner, recipient, id); + + testing::set_caller_address(owner); + ERC1155Impl::set_approval_for_all(ref state, OPERATOR(), true); + utils::drop_event(ZERO()); + + testing::set_caller_address(OPERATOR()); + ERC1155Impl::safe_transfer_from(ref state, owner, recipient, id, amount, array![]); + assert_event_transfer_single(owner, recipient, id, amount); + + assert_state_after_transfer(@state, owner, recipient, id); +} + +#[test] +#[available_gas(50000000)] +fn test_safeTransferFrom_approved_for_all() { + let mut state = setup(); + let id = TOKEN_ID; + let amount = TOKEN_AMOUNT; + let owner = OWNER(); + let recipient = RECIPIENT(); + + assert_state_before_transfer(@state, owner, recipient, id); + + testing::set_caller_address(owner); + ERC1155CamelOnlyImpl::setApprovalForAll(ref state, OPERATOR(), true); + utils::drop_event(ZERO()); + + testing::set_caller_address(OPERATOR()); + ERC1155CamelOnlyImpl::safeTransferFrom(ref state, owner, recipient, id, amount, array![]); + assert_event_transfer_single(owner, recipient, id, amount); + + assert_state_after_transfer(@state, owner, recipient, id); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC1155: unauthorized caller',))] +fn test_safe_transfer_from_unauthorized() { + let mut state = setup(); + testing::set_caller_address(OTHER()); + ERC1155Impl::safe_transfer_from( + ref state, OWNER(), RECIPIENT(), TOKEN_ID, TOKEN_AMOUNT, array![] + ); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC1155: unauthorized caller',))] +fn test_safeTransferFrom_unauthorized() { + let mut state = setup(); + testing::set_caller_address(OTHER()); + ERC1155CamelOnlyImpl::safeTransferFrom( + ref state, OWNER(), RECIPIENT(), TOKEN_ID, TOKEN_AMOUNT, array![] + ); +} + + +// +// safe_batch_transfer_from & safeBatchTransferFrom +// + +#[test] +#[available_gas(50000000)] +fn test_safe_batch_transfer_from_owner() { + let mut state = setup(); + let owner = OWNER(); + let recipient = RECIPIENT(); + + let ids = array![TOKEN_ID, TOKEN_ID_2]; + let amounts = array![TOKEN_AMOUNT, TOKEN_AMOUNT_2]; + + assert_state_before_batch_transfer(@state, owner, recipient); + + testing::set_caller_address(owner); + ERC1155Impl::safe_batch_transfer_from( + ref state, owner, recipient, ids.clone(), amounts.clone(), array![] + ); + assert_event_transfer_batch(owner, recipient, ids, amounts); + + assert_state_after_batch_transfer(@state, owner, recipient); +} + +#[test] +#[available_gas(50000000)] +fn test_safeBatchTransferFrom_owner() { + let mut state = setup(); + let owner = OWNER(); + let recipient = RECIPIENT(); + + let ids = array![TOKEN_ID, TOKEN_ID_2]; + let amounts = array![TOKEN_AMOUNT, TOKEN_AMOUNT_2]; + + assert_state_before_batch_transfer(@state, owner, recipient); + + testing::set_caller_address(owner); + ERC1155CamelOnlyImpl::safeBatchTransferFrom( + ref state, owner, recipient, ids.clone(), amounts.clone(), array![] + ); + assert_event_transfer_batch(owner, recipient, ids, amounts); + + assert_state_after_batch_transfer(@state, owner, recipient); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC1155: wrong sender',))] +fn test_safe_batch_transfer_from_zero() { + let (world, mut state) = STATE(); + + let ids = array![TOKEN_ID, TOKEN_ID_2]; + let amounts = array![TOKEN_AMOUNT, TOKEN_AMOUNT_2]; + + ERC1155Impl::safe_batch_transfer_from(ref state, ZERO(), RECIPIENT(), ids, amounts, array![]); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC1155: wrong sender',))] +fn test_safeBatchTransferFrom_zero() { + let (world, mut state) = STATE(); + + let ids = array![TOKEN_ID, TOKEN_ID_2]; + let amounts = array![TOKEN_AMOUNT, TOKEN_AMOUNT_2]; + + ERC1155CamelOnlyImpl::safeBatchTransferFrom( + ref state, ZERO(), RECIPIENT(), ids, amounts, array![] + ); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC1155: invalid receiver',))] +fn test_safe_batch_transfer_from_to_zero() { + let (world, mut state) = STATE(); + + let ids = array![TOKEN_ID, TOKEN_ID_2]; + let amounts = array![TOKEN_AMOUNT, TOKEN_AMOUNT_2]; + + ERC1155Impl::safe_batch_transfer_from(ref state, OWNER(), ZERO(), ids, amounts, array![]); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC1155: invalid receiver',))] +fn test_safeBatchTransferFrom_to_zero() { + let (world, mut state) = STATE(); + + let ids = array![TOKEN_ID, TOKEN_ID_2]; + let amounts = array![TOKEN_AMOUNT, TOKEN_AMOUNT_2]; + + ERC1155Impl::safe_batch_transfer_from(ref state, OWNER(), ZERO(), ids, amounts, array![]); +} + +#[test] +#[available_gas(50000000)] +fn test_safe_batch_transfer_from_to_owner() { + let mut state = setup(); + + let ids = array![TOKEN_ID, TOKEN_ID_2]; + let amounts = array![TOKEN_AMOUNT, TOKEN_AMOUNT_2]; + + assert( + ERC1155Impl::balance_of(@state, OWNER(), TOKEN_ID) == TOKEN_AMOUNT, + 'Balance of owner before1' + ); + assert( + ERC1155Impl::balance_of(@state, OWNER(), TOKEN_ID_2) == TOKEN_AMOUNT_2, + 'Balance of owner before1' + ); + + testing::set_caller_address(OWNER()); + ERC1155Impl::safe_batch_transfer_from( + ref state, OWNER(), OWNER(), ids.clone(), amounts.clone(), array![] + ); + assert_event_transfer_batch(OWNER(), OWNER(), ids, amounts); + + assert( + ERC1155Impl::balance_of(@state, OWNER(), TOKEN_ID) == TOKEN_AMOUNT, + 'Balance of owner after1' + ); + assert( + ERC1155Impl::balance_of(@state, OWNER(), TOKEN_ID_2) == TOKEN_AMOUNT_2, + 'Balance of owner after2' + ); +} + +#[test] +#[available_gas(50000000)] +fn test_safeBatchTransferFrom_to_owner() { + let mut state = setup(); + + let ids = array![TOKEN_ID, TOKEN_ID_2]; + let amounts = array![TOKEN_AMOUNT, TOKEN_AMOUNT_2]; + + assert( + ERC1155Impl::balance_of(@state, OWNER(), TOKEN_ID) == TOKEN_AMOUNT, + 'Balance of owner before1' + ); + assert( + ERC1155Impl::balance_of(@state, OWNER(), TOKEN_ID_2) == TOKEN_AMOUNT_2, + 'Balance of owner before1' + ); + + testing::set_caller_address(OWNER()); + ERC1155CamelOnlyImpl::safeBatchTransferFrom( + ref state, OWNER(), OWNER(), ids.clone(), amounts.clone(), array![] + ); + assert_event_transfer_batch(OWNER(), OWNER(), ids, amounts); + + assert( + ERC1155Impl::balance_of(@state, OWNER(), TOKEN_ID) == TOKEN_AMOUNT, + 'Balance of owner after1' + ); + assert( + ERC1155Impl::balance_of(@state, OWNER(), TOKEN_ID_2) == TOKEN_AMOUNT_2, + 'Balance of owner after2' + ); +} + +#[test] +#[available_gas(50000000)] +fn test_batch_transfer_from_approved_for_all() { + let mut state = setup(); + let owner = OWNER(); + let recipient = RECIPIENT(); + + let ids = array![TOKEN_ID, TOKEN_ID_2]; + let amounts = array![TOKEN_AMOUNT, TOKEN_AMOUNT_2]; + + assert_state_before_batch_transfer(@state, owner, recipient); + + testing::set_caller_address(owner); + ERC1155Impl::set_approval_for_all(ref state, OPERATOR(), true); + utils::drop_event(ZERO()); + + testing::set_caller_address(OPERATOR()); + ERC1155Impl::safe_batch_transfer_from( + ref state, owner, recipient, ids.clone(), amounts.clone(), array![] + ); + assert_event_transfer_batch(owner, recipient, ids, amounts); + + assert_state_after_batch_transfer(@state, owner, recipient); +} + +#[test] +#[available_gas(50000000)] +fn test_safeBatchTransferFrom_approved_for_all() { + let mut state = setup(); + let owner = OWNER(); + let recipient = RECIPIENT(); + + let ids = array![TOKEN_ID, TOKEN_ID_2]; + let amounts = array![TOKEN_AMOUNT, TOKEN_AMOUNT_2]; + + assert_state_before_batch_transfer(@state, owner, recipient); + + testing::set_caller_address(owner); + ERC1155CamelOnlyImpl::setApprovalForAll(ref state, OPERATOR(), true); + utils::drop_event(ZERO()); + + testing::set_caller_address(OPERATOR()); + ERC1155CamelOnlyImpl::safeBatchTransferFrom( + ref state, owner, recipient, ids.clone(), amounts.clone(), array![] + ); + assert_event_transfer_batch(owner, recipient, ids, amounts); + + assert_state_after_batch_transfer(@state, owner, recipient); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC1155: unauthorized caller',))] +fn test_safe_batch_transfer_from_unauthorized() { + let mut state = setup(); + let ids = array![TOKEN_ID, TOKEN_ID_2]; + let amounts = array![TOKEN_AMOUNT, TOKEN_AMOUNT_2]; + + testing::set_caller_address(OTHER()); + ERC1155Impl::safe_batch_transfer_from(ref state, OWNER(), RECIPIENT(), ids, amounts, array![]); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC1155: unauthorized caller',))] +fn test_safeBatchTransferFrom_unauthorized() { + let mut state = setup(); + let ids = array![TOKEN_ID, TOKEN_ID_2]; + let amounts = array![TOKEN_AMOUNT, TOKEN_AMOUNT_2]; + + testing::set_caller_address(OTHER()); + ERC1155CamelOnlyImpl::safeBatchTransferFrom( + ref state, OWNER(), RECIPIENT(), ids, amounts, array![] + ); +} + +// +// _mint +// + +#[test] +#[available_gas(20000000)] +fn test__mint() { + let (world, mut state) = STATE(); + let recipient = RECIPIENT(); + + assert( + ERC1155Impl::balance_of(@state, recipient, TOKEN_ID_2) == 0, 'Balance of recipient before' + ); + + InternalImpl::_mint(ref state, recipient, TOKEN_ID_2, TOKEN_AMOUNT_2); + assert_event_transfer_single(ZERO(), recipient, TOKEN_ID_2, TOKEN_AMOUNT_2); + + assert( + ERC1155Impl::balance_of(@state, recipient, TOKEN_ID_2) == TOKEN_AMOUNT_2, + 'Balance of recipient after' + ); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC1155: invalid receiver',))] +fn test__mint_to_zero() { + let (world, mut state) = STATE(); + InternalImpl::_mint(ref state, ZERO(), TOKEN_ID, TOKEN_AMOUNT); +} + + +// +// _burn +// + +#[test] +#[available_gas(20000000)] +fn test__burn() { + let mut state = setup(); + + assert( + ERC1155Impl::balance_of(@state, OWNER(), TOKEN_ID) == TOKEN_AMOUNT, + 'Balance of owner before' + ); + + testing::set_caller_address(OWNER()); + InternalImpl::_burn(ref state, TOKEN_ID, 2); + assert_event_transfer_single(OWNER(), ZERO(), TOKEN_ID, 2); + + assert( + ERC1155Impl::balance_of(@state, OWNER(), TOKEN_ID) == TOKEN_AMOUNT - 2, + 'Balance of owner after' + ); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC1155: insufficient balance',))] +fn test__burn_more_than_balance() { + let mut state = setup(); + + assert( + ERC1155Impl::balance_of(@state, OWNER(), TOKEN_ID) == TOKEN_AMOUNT, + 'Balance of owner before' + ); + InternalImpl::_burn(ref state, TOKEN_ID, TOKEN_AMOUNT + 1); +} + + +// +// Helpers +// + +fn assert_state_before_transfer( + state: @ERC1155::ContractState, owner: ContractAddress, recipient: ContractAddress, id: u256, +) { + assert(ERC1155Impl::balance_of(state, owner, id) == TOKEN_AMOUNT, 'Balance of owner before'); + assert(ERC1155Impl::balance_of(state, recipient, id) == 0, 'Balance of recipient before'); +} + +fn assert_state_after_transfer( + state: @ERC1155::ContractState, owner: ContractAddress, recipient: ContractAddress, id: u256 +) { + assert(ERC1155Impl::balance_of(state, owner, id) == 0, 'Balance of owner after'); + assert( + ERC1155Impl::balance_of(state, recipient, id) == TOKEN_AMOUNT, 'Balance of recipient after' + ); +} + +fn assert_state_before_batch_transfer( + state: @ERC1155::ContractState, owner: ContractAddress, recipient: ContractAddress +) { + assert( + ERC1155Impl::balance_of(state, owner, TOKEN_ID) == TOKEN_AMOUNT, 'Balance of owner before1' + ); + assert( + ERC1155Impl::balance_of(state, owner, TOKEN_ID_2) == TOKEN_AMOUNT_2, + 'Balance of owner before2' + ); + assert( + ERC1155Impl::balance_of(state, recipient, TOKEN_ID) == 0, 'Balance of recipient before1' + ); + assert( + ERC1155Impl::balance_of(state, recipient, TOKEN_ID_2) == 0, 'Balance of recipient before2' + ); +} + +fn assert_state_after_batch_transfer( + state: @ERC1155::ContractState, owner: ContractAddress, recipient: ContractAddress +) { + assert(ERC1155Impl::balance_of(state, owner, TOKEN_ID) == 0, 'Balance of owner after1'); + assert(ERC1155Impl::balance_of(state, owner, TOKEN_ID_2) == 0, 'Balance of owner after2'); + assert( + ERC1155Impl::balance_of(state, recipient, TOKEN_ID) == TOKEN_AMOUNT, + 'Balance of recipient after1' + ); + assert( + ERC1155Impl::balance_of(state, recipient, TOKEN_ID_2) == TOKEN_AMOUNT_2, + 'Balance of recipient after2' + ); +} + + +// +// events +// + +fn assert_event_approval_for_all( + owner: ContractAddress, operator: ContractAddress, approved: bool +) { + let event = utils::pop_log::(ZERO()).unwrap(); + assert(event.owner == owner, 'Invalid `owner`'); + assert(event.operator == operator, 'Invalid `operator`'); + assert(event.approved == approved, 'Invalid `approved`'); + utils::assert_no_events_left(ZERO()); +} + +fn assert_event_transfer_single( + from: ContractAddress, to: ContractAddress, id: u256, amount: u256 +) { + let event = utils::pop_log::(ZERO()).unwrap(); + assert(event.from == from, 'Invalid `from`'); + assert(event.to == to, 'Invalid `to`'); + assert(event.id == id, 'Invalid `id`'); + assert(event.value == amount, 'Invalid `amount`'); + utils::assert_no_events_left(ZERO()); +} + +fn assert_event_transfer_batch( + from: ContractAddress, to: ContractAddress, ids: Array, amounts: Array +) { + let event = utils::pop_log::(ZERO()).unwrap(); + assert(event.from == from, 'Invalid `from`'); + assert(event.to == to, 'Invalid `to`'); + assert(event.ids.len() == event.values.len(), 'Invalid array length'); + + let mut i = 0; + + loop { + if i == event.ids.len() { + break; + } + + assert(event.ids.at(i) == ids.at(i), 'Invalid `id`'); + assert(event.values.at(i) == amounts.at(i), 'Invalid `id`'); + + i += 1; + }; + + utils::assert_no_events_left(ZERO()); +} + diff --git a/presets/src/erc20/erc20.cairo b/presets/src/erc20/erc20.cairo new file mode 100644 index 00000000..d21be836 --- /dev/null +++ b/presets/src/erc20/erc20.cairo @@ -0,0 +1,312 @@ +// External imports + +use openzeppelin::token::erc20::interface; + +#[starknet::contract] +mod ERC20 { + use dojo_erc::token::erc20_models::{ERC20Allowance, ERC20Balance, ERC20Meta}; + use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait}; + use integer::BoundedInt; + use starknet::ContractAddress; + use starknet::{get_caller_address, get_contract_address}; + use zeroable::Zeroable; + use debug::PrintTrait; + + // Local imports + + use super::interface::{IERC20, IERC20CamelOnly}; + + + #[storage] + struct Storage { + _world: ContractAddress, + } + + #[event] + #[derive(Copy, Drop, starknet::Event)] + enum Event { + Transfer: Transfer, + Approval: Approval, + } + + #[derive(Copy, Drop, starknet::Event)] + struct Transfer { + from: ContractAddress, + to: ContractAddress, + value: u256 + } + + #[derive(Copy, Drop, starknet::Event)] + struct Approval { + owner: ContractAddress, + spender: ContractAddress, + value: u256 + } + + mod Errors { + const APPROVE_FROM_ZERO: felt252 = 'ERC20: approve from 0'; + const APPROVE_TO_ZERO: felt252 = 'ERC20: approve to 0'; + const TRANSFER_FROM_ZERO: felt252 = 'ERC20: transfer from 0'; + const TRANSFER_TO_ZERO: felt252 = 'ERC20: transfer to 0'; + const BURN_FROM_ZERO: felt252 = 'ERC20: burn from 0'; + const MINT_TO_ZERO: felt252 = 'ERC20: mint to 0'; + } + + #[constructor] + fn constructor( + ref self: ContractState, + world: ContractAddress, + name: felt252, + symbol: felt252, + initial_supply: u256, + recipient: ContractAddress + ) { + self._world.write(world); + self.initializer(name, symbol); + self._mint(recipient, initial_supply); + } + + // + // External + // + + #[external(v0)] + impl ERC20Impl of IERC20 { + fn name(self: @ContractState) -> felt252 { + self.get_meta().name + } + + fn symbol(self: @ContractState) -> felt252 { + self.get_meta().symbol + } + + fn decimals(self: @ContractState) -> u8 { + 18 + } + + fn total_supply(self: @ContractState) -> u256 { + self.get_meta().total_supply + } + + fn balance_of(self: @ContractState, account: ContractAddress) -> u256 { + self.get_balance(account).amount + } + + fn allowance( + self: @ContractState, owner: ContractAddress, spender: ContractAddress + ) -> u256 { + self.get_allowance(owner, spender).amount + } + + fn transfer(ref self: ContractState, recipient: ContractAddress, amount: u256) -> bool { + let sender = get_caller_address(); + self._transfer(sender, recipient, amount); + true + } + + fn transfer_from( + ref self: ContractState, + sender: ContractAddress, + recipient: ContractAddress, + amount: u256 + ) -> bool { + let caller = get_caller_address(); + self._spend_allowance(sender, caller, amount); + self._transfer(sender, recipient, amount); + true + } + + fn approve(ref self: ContractState, spender: ContractAddress, amount: u256) -> bool { + let owner = get_caller_address(); + self + .set_allowance( + ERC20Allowance { token: get_contract_address(), owner, spender, amount } + ); + true + } + } + + #[external(v0)] + impl ERC20CamelOnlyImpl of IERC20CamelOnly { + fn totalSupply(self: @ContractState) -> u256 { + ERC20Impl::total_supply(self) + } + + fn balanceOf(self: @ContractState, account: ContractAddress) -> u256 { + ERC20Impl::balance_of(self, account) + } + + fn transferFrom( + ref self: ContractState, + sender: ContractAddress, + recipient: ContractAddress, + amount: u256 + ) -> bool { + ERC20Impl::transfer_from(ref self, sender, recipient, amount) + } + } + + #[external(v0)] + fn increase_allowance( + ref self: ContractState, spender: ContractAddress, added_value: u256 + ) -> bool { + self.update_allowance(get_caller_address(), spender, 0, added_value); + true + } + + #[external(v0)] + fn increaseAllowance( + ref self: ContractState, spender: ContractAddress, addedValue: u256 + ) -> bool { + increase_allowance(ref self, spender, addedValue) + } + + #[external(v0)] + fn decrease_allowance( + ref self: ContractState, spender: ContractAddress, subtracted_value: u256 + ) -> bool { + self.update_allowance(get_caller_address(), spender, subtracted_value, 0); + true + } + + #[external(v0)] + fn decreaseAllowance( + ref self: ContractState, spender: ContractAddress, subtractedValue: u256 + ) -> bool { + decrease_allowance(ref self, spender, subtractedValue) + } + + // + // Internal + // + + #[generate_trait] + impl WorldInteractionsImpl of WorldInteractionsTrait { + fn world(self: @ContractState) -> IWorldDispatcher { + IWorldDispatcher { contract_address: self._world.read() } + } + + fn get_meta(self: @ContractState) -> ERC20Meta { + get!(self.world(), get_contract_address(), ERC20Meta) + } + + // Helper function to update total_supply model + fn update_total_supply(ref self: ContractState, subtract: u256, add: u256) { + let mut meta = self.get_meta(); + // adding and subtracting is fewer steps than if + meta.total_supply = meta.total_supply - subtract; + meta.total_supply = meta.total_supply + add; + set!(self.world(), (meta)); + } + + // Helper function for balance model + fn get_balance(self: @ContractState, account: ContractAddress) -> ERC20Balance { + get!(self.world(), (get_contract_address(), account), ERC20Balance) + } + + fn update_balance( + ref self: ContractState, account: ContractAddress, subtract: u256, add: u256 + ) { + let mut balance: ERC20Balance = self.get_balance(account); + // adding and subtracting is fewer steps than if + balance.amount = balance.amount - subtract; + balance.amount = balance.amount + add; + set!(self.world(), (balance)); + } + + // Helper function for allowance model + fn get_allowance( + self: @ContractState, owner: ContractAddress, spender: ContractAddress, + ) -> ERC20Allowance { + get!(self.world(), (get_contract_address(), owner, spender), ERC20Allowance) + } + + fn update_allowance( + ref self: ContractState, + owner: ContractAddress, + spender: ContractAddress, + subtract: u256, + add: u256 + ) { + let mut allowance = self.get_allowance(owner, spender); + // adding and subtracting is fewer steps than if + allowance.amount = allowance.amount - subtract; + allowance.amount = allowance.amount + add; + self.set_allowance(allowance); + } + + fn set_allowance(ref self: ContractState, allowance: ERC20Allowance) { + assert(!allowance.owner.is_zero(), Errors::APPROVE_FROM_ZERO); + assert(!allowance.spender.is_zero(), Errors::APPROVE_TO_ZERO); + set!(self.world(), (allowance)); + self + .emit_event( + Approval { + owner: allowance.owner, spender: allowance.spender, value: allowance.amount + } + ); + } + + fn emit_event< + S, impl IntoImp: traits::Into, impl SDrop: Drop, impl SCopy: Copy + >( + ref self: ContractState, event: S + ) { + self.emit(event); + emit!(self.world(), event); + } + } + + #[generate_trait] + impl InternalImpl of InternalTrait { + fn initializer(ref self: ContractState, name: felt252, symbol: felt252) { + let meta = ERC20Meta { token: get_contract_address(), name, symbol, total_supply: 0 }; + set!(self.world(), (meta)); + } + + fn _mint(ref self: ContractState, recipient: ContractAddress, amount: u256) { + assert(!recipient.is_zero(), Errors::MINT_TO_ZERO); + self.update_total_supply(0, amount); + self.update_balance(recipient, 0, amount); + self.emit_event(Transfer { from: Zeroable::zero(), to: recipient, value: amount }); + } + + fn _burn(ref self: ContractState, account: ContractAddress, amount: u256) { + assert(!account.is_zero(), Errors::BURN_FROM_ZERO); + self.update_total_supply(amount, 0); + self.update_balance(account, amount, 0); + self.emit_event(Transfer { from: account, to: Zeroable::zero(), value: amount }); + } + + fn _approve( + ref self: ContractState, owner: ContractAddress, spender: ContractAddress, amount: u256 + ) { + self + .set_allowance( + ERC20Allowance { token: get_contract_address(), owner, spender, amount } + ); + } + + fn _transfer( + ref self: ContractState, + sender: ContractAddress, + recipient: ContractAddress, + amount: u256 + ) { + assert(!sender.is_zero(), Errors::TRANSFER_FROM_ZERO); + assert(!recipient.is_zero(), Errors::TRANSFER_TO_ZERO); + self.update_balance(sender, amount, 0); + self.update_balance(recipient, 0, amount); + self.emit_event(Transfer { from: sender, to: recipient, value: amount }); + } + + fn _spend_allowance( + ref self: ContractState, owner: ContractAddress, spender: ContractAddress, amount: u256 + ) { + let current_allowance = self.get_allowance(owner, spender).amount; + if current_allowance != BoundedInt::max() { + self.update_allowance(owner, spender, amount, 0); + } + } + } +} diff --git a/presets/src/erc20/models.cairo b/presets/src/erc20/models.cairo new file mode 100644 index 00000000..f19cda5c --- /dev/null +++ b/presets/src/erc20/models.cairo @@ -0,0 +1,28 @@ +#[derive(Model, Copy, Drop, Serde)] +struct ERC20Balance { + #[key] + token: starknet::ContractAddress, + #[key] + account: starknet::ContractAddress, + amount: u256, +} + +#[derive(Model, Copy, Drop, Serde)] +struct ERC20Allowance { + #[key] + token: starknet::ContractAddress, + #[key] + owner: starknet::ContractAddress, + #[key] + spender: starknet::ContractAddress, + amount: u256, +} + +#[derive(Model, Copy, Drop, Serde)] +struct ERC20Meta { + #[key] + token: starknet::ContractAddress, + name: felt252, + symbol: felt252, + total_supply: u256, +} diff --git a/presets/src/erc20/tests.cairo b/presets/src/erc20/tests.cairo new file mode 100644 index 00000000..669cfc7c --- /dev/null +++ b/presets/src/erc20/tests.cairo @@ -0,0 +1,546 @@ +use integer::BoundedInt; +use integer::u256; +use integer::u256_from_felt252; +use dojo_erc::tests::utils; +use dojo_erc::tests::constants::{ + ZERO, OWNER, SPENDER, RECIPIENT, NAME, SYMBOL, DECIMALS, SUPPLY, VALUE +}; +use dojo_erc::token::erc20::ERC20::Approval; +use dojo_erc::token::erc20::ERC20::ERC20Impl; +use dojo_erc::token::erc20::ERC20::InternalImpl; +use dojo_erc::token::erc20::ERC20::Transfer; +use dojo_erc::token::erc20::ERC20; +use starknet::ContractAddress; +use starknet::contract_address_const; +use starknet::testing; +use zeroable::Zeroable; +use dojo::test_utils::spawn_test_world; +use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait}; + +use dojo_erc::token::erc20_models::{ + ERC20Allowance, erc_20_allowance, ERC20Balance, erc_20_balance, ERC20Meta, erc_20_meta +}; +use dojo_erc::token::erc20::ERC20::_worldContractMemberStateTrait; +use debug::PrintTrait; + +// +// Setup +// + +fn STATE() -> (IWorldDispatcher, ERC20::ContractState) { + let world = spawn_test_world( + array![ + erc_20_allowance::TEST_CLASS_HASH, + erc_20_balance::TEST_CLASS_HASH, + erc_20_meta::TEST_CLASS_HASH, + ] + ); + let mut state = ERC20::contract_state_for_testing(); + state._world.write(world.contract_address); + (world, state) +} + +fn setup() -> ERC20::ContractState { + let (world, mut state) = STATE(); + ERC20::constructor(ref state, world.contract_address, NAME, SYMBOL, SUPPLY, OWNER()); + utils::drop_event(ZERO()); + state +} + +// +// initializer & constructor +// + +#[test] +#[available_gas(25000000)] +fn test_initializer() { + let (world, mut state) = STATE(); + InternalImpl::initializer(ref state, NAME, SYMBOL); + + assert(ERC20Impl::name(@state) == NAME, 'Name should be NAME'); + assert(ERC20Impl::symbol(@state) == SYMBOL, 'Symbol should be SYMBOL'); + assert(ERC20Impl::decimals(@state) == DECIMALS, 'Decimals should be 18'); + assert(ERC20Impl::total_supply(@state) == 0, 'Supply should eq 0'); +} + + +#[test] +#[available_gas(25000000)] +fn test_constructor() { + let (world, mut state) = STATE(); + ERC20::constructor(ref state, world.contract_address, NAME, SYMBOL, SUPPLY, OWNER()); + + assert_only_event_transfer(ZERO(), OWNER(), SUPPLY); + + assert(ERC20Impl::balance_of(@state, OWNER()) == SUPPLY, 'Should eq inital_supply'); + assert(ERC20Impl::total_supply(@state) == SUPPLY, 'Should eq inital_supply'); + assert(ERC20Impl::name(@state) == NAME, 'Name should be NAME'); + assert(ERC20Impl::symbol(@state) == SYMBOL, 'Symbol should be SYMBOL'); + assert(ERC20Impl::decimals(@state) == DECIMALS, 'Decimals should be 18'); +} + +// +// Getters +// + +#[test] +#[available_gas(25000000)] +fn test_total_supply() { + let (world, mut state) = STATE(); + InternalImpl::_mint(ref state, OWNER(), SUPPLY); + assert(ERC20Impl::total_supply(@state) == SUPPLY, 'Should eq SUPPLY'); +} + +#[test] +#[available_gas(25000000)] +fn test_balance_of() { + let (world, mut state) = STATE(); + InternalImpl::_mint(ref state, OWNER(), SUPPLY); + assert(ERC20Impl::balance_of(@state, OWNER()) == SUPPLY, 'Should eq SUPPLY'); +} + + +#[test] +#[available_gas(25000000)] +fn test_allowance() { + let mut state = setup(); + testing::set_caller_address(OWNER()); + ERC20Impl::approve(ref state, SPENDER(), VALUE); + + assert(ERC20Impl::allowance(@state, OWNER(), SPENDER()) == VALUE, 'Should eq VALUE'); +} + +// +// approve & _approve +// + +#[test] +#[available_gas(25000000)] +fn test_approve() { + let mut state = setup(); + testing::set_caller_address(OWNER()); + assert(ERC20Impl::approve(ref state, SPENDER(), VALUE), 'Should return true'); + + assert_only_event_approval(OWNER(), SPENDER(), VALUE); + assert( + ERC20Impl::allowance(@state, OWNER(), SPENDER()) == VALUE, 'Spender not approved correctly' + ); +} + +#[test] +#[available_gas(25000000)] +#[should_panic(expected: ('ERC20: approve from 0',))] +fn test_approve_from_zero() { + let mut state = setup(); + ERC20Impl::approve(ref state, SPENDER(), VALUE); +} + +#[test] +#[available_gas(25000000)] +#[should_panic(expected: ('ERC20: approve to 0',))] +fn test_approve_to_zero() { + let mut state = setup(); + testing::set_caller_address(OWNER()); + ERC20Impl::approve(ref state, Zeroable::zero(), VALUE); +} + +#[test] +#[available_gas(25000000)] +fn test__approve() { + let mut state = setup(); + testing::set_caller_address(OWNER()); + InternalImpl::_approve(ref state, OWNER(), SPENDER(), VALUE); + + assert_only_event_approval(OWNER(), SPENDER(), VALUE); + assert( + ERC20Impl::allowance(@state, OWNER(), SPENDER()) == VALUE, 'Spender not approved correctly' + ); +} + +#[test] +#[available_gas(25000000)] +#[should_panic(expected: ('ERC20: approve from 0',))] +fn test__approve_from_zero() { + let mut state = setup(); + InternalImpl::_approve(ref state, Zeroable::zero(), SPENDER(), VALUE); +} + +#[test] +#[available_gas(25000000)] +#[should_panic(expected: ('ERC20: approve to 0',))] +fn test__approve_to_zero() { + let mut state = setup(); + testing::set_caller_address(OWNER()); + InternalImpl::_approve(ref state, OWNER(), Zeroable::zero(), VALUE); +} + +// +// transfer & _transfer +// + +#[test] +#[available_gas(25000000)] +fn test_transfer() { + let mut state = setup(); + testing::set_caller_address(OWNER()); + assert(ERC20Impl::transfer(ref state, RECIPIENT(), VALUE), 'Should return true'); + + assert_only_event_transfer(OWNER(), RECIPIENT(), VALUE); + assert(ERC20Impl::balance_of(@state, RECIPIENT()) == VALUE, 'Balance should eq VALUE'); + assert(ERC20Impl::balance_of(@state, OWNER()) == SUPPLY - VALUE, 'Should eq supply - VALUE'); + assert(ERC20Impl::total_supply(@state) == SUPPLY, 'Total supply should not change'); +} + +#[test] +#[available_gas(25000000)] +fn test__transfer() { + let mut state = setup(); + + InternalImpl::_transfer(ref state, OWNER(), RECIPIENT(), VALUE); + + assert_only_event_transfer(OWNER(), RECIPIENT(), VALUE); + assert(ERC20Impl::balance_of(@state, RECIPIENT()) == VALUE, 'Balance should eq amount'); + assert(ERC20Impl::balance_of(@state, OWNER()) == SUPPLY - VALUE, 'Should eq supply - amount'); + assert(ERC20Impl::total_supply(@state) == SUPPLY, 'Total supply should not change'); +} + +#[test] +#[available_gas(25000000)] +#[should_panic(expected: ('u256_sub Overflow',))] +fn test__transfer_not_enough_balance() { + let mut state = setup(); + testing::set_caller_address(OWNER()); + + let balance_plus_one = SUPPLY + 1; + InternalImpl::_transfer(ref state, OWNER(), RECIPIENT(), balance_plus_one); +} + +#[test] +#[available_gas(25000000)] +#[should_panic(expected: ('ERC20: transfer from 0',))] +fn test__transfer_from_zero() { + let mut state = setup(); + InternalImpl::_transfer(ref state, Zeroable::zero(), RECIPIENT(), VALUE); +} + +#[test] +#[available_gas(25000000)] +#[should_panic(expected: ('ERC20: transfer to 0',))] +fn test__transfer_to_zero() { + let mut state = setup(); + InternalImpl::_transfer(ref state, OWNER(), Zeroable::zero(), VALUE); +} + +// +// transfer_from +// + +#[test] +#[available_gas(30000000)] +fn test_transfer_from() { + let mut state = setup(); + testing::set_caller_address(OWNER()); + ERC20Impl::approve(ref state, SPENDER(), VALUE); + utils::drop_event(ZERO()); + + testing::set_caller_address(SPENDER()); + assert(ERC20Impl::transfer_from(ref state, OWNER(), RECIPIENT(), VALUE), 'Should return true'); + + assert_event_approval(OWNER(), SPENDER(), 0); + assert_only_event_transfer(OWNER(), RECIPIENT(), VALUE); + + assert(ERC20Impl::balance_of(@state, RECIPIENT()) == VALUE, 'Should eq amount'); + assert(ERC20Impl::balance_of(@state, OWNER()) == SUPPLY - VALUE, 'Should eq suppy - amount'); + assert(ERC20Impl::allowance(@state, OWNER(), SPENDER()) == 0, 'Should eq 0'); + assert(ERC20Impl::total_supply(@state) == SUPPLY, 'Total supply should not change'); +} + +#[test] +#[available_gas(25000000)] +fn test_transfer_from_doesnt_consume_infinite_allowance() { + let mut state = setup(); + testing::set_caller_address(OWNER()); + ERC20Impl::approve(ref state, SPENDER(), BoundedInt::max()); + + testing::set_caller_address(SPENDER()); + ERC20Impl::transfer_from(ref state, OWNER(), RECIPIENT(), VALUE); + + assert( + ERC20Impl::allowance(@state, OWNER(), SPENDER()) == BoundedInt::max(), + 'Allowance should not change' + ); +} + +#[test] +#[available_gas(25000000)] +#[should_panic(expected: ('u256_sub Overflow',))] +fn test_transfer_from_greater_than_allowance() { + let mut state = setup(); + testing::set_caller_address(OWNER()); + ERC20Impl::approve(ref state, SPENDER(), VALUE); + + testing::set_caller_address(SPENDER()); + let allowance_plus_one = VALUE + 1; + ERC20Impl::transfer_from(ref state, OWNER(), RECIPIENT(), allowance_plus_one); +} + +#[test] +#[available_gas(25000000)] +#[should_panic(expected: ('ERC20: transfer to 0',))] +fn test_transfer_from_to_zero_address() { + let mut state = setup(); + testing::set_caller_address(OWNER()); + ERC20Impl::approve(ref state, SPENDER(), VALUE); + + testing::set_caller_address(SPENDER()); + ERC20Impl::transfer_from(ref state, OWNER(), Zeroable::zero(), VALUE); +} + +#[test] +#[available_gas(25000000)] +#[should_panic(expected: ('u256_sub Overflow',))] +fn test_transfer_from_from_zero_address() { + let mut state = setup(); + ERC20Impl::transfer_from(ref state, Zeroable::zero(), RECIPIENT(), VALUE); +} + +// +// increase_allowance & increaseAllowance +// + +#[test] +#[available_gas(25000000)] +fn test_increase_allowance() { + let mut state = setup(); + testing::set_caller_address(OWNER()); + ERC20Impl::approve(ref state, SPENDER(), VALUE); + utils::drop_event(ZERO()); + + assert(ERC20::increase_allowance(ref state, SPENDER(), VALUE), 'Should return true'); + + assert_only_event_approval(OWNER(), SPENDER(), VALUE * 2); + assert(ERC20Impl::allowance(@state, OWNER(), SPENDER()) == VALUE * 2, 'Should be amount * 2'); +} + +#[test] +#[available_gas(25000000)] +#[should_panic(expected: ('ERC20: approve to 0',))] +fn test_increase_allowance_to_zero_address() { + let mut state = setup(); + testing::set_caller_address(OWNER()); + ERC20::increase_allowance(ref state, Zeroable::zero(), VALUE); +} + +#[test] +#[available_gas(25000000)] +#[should_panic(expected: ('ERC20: approve from 0',))] +fn test_increase_allowance_from_zero_address() { + let mut state = setup(); + ERC20::increase_allowance(ref state, SPENDER(), VALUE); +} + +#[test] +#[available_gas(25000000)] +fn test_increaseAllowance() { + let mut state = setup(); + testing::set_caller_address(OWNER()); + ERC20Impl::approve(ref state, SPENDER(), VALUE); + utils::drop_event(ZERO()); + + assert(ERC20::increaseAllowance(ref state, SPENDER(), VALUE), 'Should return true'); + + assert_only_event_approval(OWNER(), SPENDER(), 2 * VALUE); + assert(ERC20Impl::allowance(@state, OWNER(), SPENDER()) == VALUE * 2, 'Should be amount * 2'); +} + +#[test] +#[available_gas(25000000)] +#[should_panic(expected: ('ERC20: approve to 0',))] +fn test_increaseAllowance_to_zero_address() { + let mut state = setup(); + testing::set_caller_address(OWNER()); + ERC20::increaseAllowance(ref state, Zeroable::zero(), VALUE); +} + +#[test] +#[available_gas(25000000)] +#[should_panic(expected: ('ERC20: approve from 0',))] +fn test_increaseAllowance_from_zero_address() { + let mut state = setup(); + ERC20::increaseAllowance(ref state, SPENDER(), VALUE); +} + +// +// decrease_allowance & decreaseAllowance +// + +#[test] +#[available_gas(25000000)] +fn test_decrease_allowance() { + let mut state = setup(); + testing::set_caller_address(OWNER()); + ERC20Impl::approve(ref state, SPENDER(), VALUE); + utils::drop_event(ZERO()); + + assert(ERC20::decrease_allowance(ref state, SPENDER(), VALUE), 'Should return true'); + + assert_only_event_approval(OWNER(), SPENDER(), 0); + assert(ERC20Impl::allowance(@state, OWNER(), SPENDER()) == VALUE - VALUE, 'Should be 0'); +} + +#[test] +#[available_gas(25000000)] +#[should_panic(expected: ('u256_sub Overflow',))] +fn test_decrease_allowance_to_zero_address() { + let mut state = setup(); + testing::set_caller_address(OWNER()); + ERC20::decrease_allowance(ref state, Zeroable::zero(), VALUE); +} + +#[test] +#[available_gas(25000000)] +#[should_panic(expected: ('u256_sub Overflow',))] +fn test_decrease_allowance_from_zero_address() { + let mut state = setup(); + ERC20::decrease_allowance(ref state, SPENDER(), VALUE); +} + +#[test] +#[available_gas(25000000)] +fn test_decreaseAllowance() { + let mut state = setup(); + testing::set_caller_address(OWNER()); + ERC20Impl::approve(ref state, SPENDER(), VALUE); + utils::drop_event(ZERO()); + + assert(ERC20::decreaseAllowance(ref state, SPENDER(), VALUE), 'Should return true'); + + assert_only_event_approval(OWNER(), SPENDER(), 0); + assert(ERC20Impl::allowance(@state, OWNER(), SPENDER()) == VALUE - VALUE, 'Should be 0'); +} + +#[test] +#[available_gas(25000000)] +#[should_panic(expected: ('u256_sub Overflow',))] +fn test_decreaseAllowance_to_zero_address() { + let mut state = setup(); + testing::set_caller_address(OWNER()); + ERC20::decreaseAllowance(ref state, Zeroable::zero(), VALUE); +} + +#[test] +#[available_gas(25000000)] +#[should_panic(expected: ('u256_sub Overflow',))] +fn test_decreaseAllowance_from_zero_address() { + let mut state = setup(); + ERC20::decreaseAllowance(ref state, SPENDER(), VALUE); +} + +// +// _spend_allowance +// + +#[test] +#[available_gas(25000000)] +fn test__spend_allowance_not_unlimited() { + let mut state = setup(); + + InternalImpl::_approve(ref state, OWNER(), SPENDER(), SUPPLY); + utils::drop_event(ZERO()); + + InternalImpl::_spend_allowance(ref state, OWNER(), SPENDER(), VALUE); + + assert_only_event_approval(OWNER(), SPENDER(), SUPPLY - VALUE); + assert( + ERC20Impl::allowance(@state, OWNER(), SPENDER()) == SUPPLY - VALUE, + 'Should eq supply - amount' + ); +} + +#[test] +#[available_gas(25000000)] +fn test__spend_allowance_unlimited() { + let mut state = setup(); + InternalImpl::_approve(ref state, OWNER(), SPENDER(), BoundedInt::max()); + + let max_minus_one: u256 = BoundedInt::max() - 1; + InternalImpl::_spend_allowance(ref state, OWNER(), SPENDER(), max_minus_one); + + assert( + ERC20Impl::allowance(@state, OWNER(), SPENDER()) == BoundedInt::max(), + 'Allowance should not change' + ); +} + +// +// _mint +// + +#[test] +#[available_gas(25000000)] +fn test__mint() { + let (world, mut state) = STATE(); + InternalImpl::_mint(ref state, OWNER(), VALUE); + assert_only_event_transfer(ZERO(), OWNER(), VALUE); + assert(ERC20Impl::balance_of(@state, OWNER()) == VALUE, 'Should eq amount'); + assert(ERC20Impl::total_supply(@state) == VALUE, 'Should eq total supply'); +} + +#[test] +#[available_gas(25000000)] +#[should_panic(expected: ('ERC20: mint to 0',))] +fn test__mint_to_zero() { + let (world, mut state) = STATE(); + InternalImpl::_mint(ref state, Zeroable::zero(), VALUE); +} + +// +// _burn +// + +#[test] +#[available_gas(25000000)] +fn test__burn() { + let mut state = setup(); + InternalImpl::_burn(ref state, OWNER(), VALUE); + + assert_only_event_transfer(OWNER(), ZERO(), VALUE); + assert(ERC20Impl::total_supply(@state) == SUPPLY - VALUE, 'Should eq supply - amount'); + assert(ERC20Impl::balance_of(@state, OWNER()) == SUPPLY - VALUE, 'Should eq supply - amount'); +} + +#[test] +#[available_gas(25000000)] +#[should_panic(expected: ('ERC20: burn from 0',))] +fn test__burn_from_zero() { + let mut state = setup(); + InternalImpl::_burn(ref state, Zeroable::zero(), VALUE); +} + +// +// Helpers +// + +fn assert_event_approval(owner: ContractAddress, spender: ContractAddress, value: u256) { + let event = utils::pop_log::(ZERO()).unwrap(); + assert(event.owner == owner, 'Invalid `owner`'); + assert(event.spender == spender, 'Invalid `spender`'); + assert(event.value == value, 'Invalid `value`'); +} + +fn assert_only_event_approval(owner: ContractAddress, spender: ContractAddress, value: u256) { + assert_event_approval(owner, spender, value); + utils::assert_no_events_left(ZERO()); +} + +fn assert_event_transfer(from: ContractAddress, to: ContractAddress, value: u256) { + let event = utils::pop_log::(ZERO()).unwrap(); + assert(event.from == from, 'Invalid `from`'); + assert(event.to == to, 'Invalid `to`'); + assert(event.value == value, 'Invalid `value`'); +} + +fn assert_only_event_transfer(from: ContractAddress, to: ContractAddress, value: u256) { + assert_event_transfer(from, to, value); + utils::assert_no_events_left(ZERO()); +} diff --git a/presets/src/erc721/erc721.cairo b/presets/src/erc721/erc721.cairo new file mode 100644 index 00000000..57f84720 --- /dev/null +++ b/presets/src/erc721/erc721.cairo @@ -0,0 +1,422 @@ +// External imports + +use openzeppelin::token::erc721::interface; + +#[starknet::contract] +mod ERC721 { + // Core imports + + use integer::BoundedInt; + use zeroable::Zeroable; + + // Starknet imports + + use starknet::ContractAddress; + use starknet::{get_caller_address, get_contract_address}; + + // Dojo imports + + use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait}; + + // External imports + + use openzeppelin::token::erc721::erc721::ERC721; + use openzeppelin::introspection::interface::{ISRC5, ISRC5Camel}; + use openzeppelin::introspection::src5::SRC5Component; + + // Internal imports + + use presets::erc721::models::{ + ERC721Meta, + ERC721OperatorApproval, + ERC721Owner, + ERC721Balance, + ERC721TokenApproval, + }; + + // Local imports + + use super::interface; + + // Components + + component!(path: SRC5Component, storage: src5, event: SRC5Event); + #[abi(embed_v0)] + impl SRC5Impl = SRC5Component::SRC5Impl; + #[abi(embed_v0)] + impl SRC5CamelImpl = SRC5Component::SRC5CamelImpl; + impl SRC5InternalImpl = SRC5Component::InternalImpl; + impl SRC5EventCopy of Copy {} + + // Storage + + #[storage] + struct Storage { + _world: ContractAddress, + #[substorage(v0)] + src5: SRC5Component::Storage + } + + // Events + + #[event] + #[derive(Copy, Drop, starknet::Event)] + enum Event { + Transfer: Transfer, + Approval: Approval, + ApprovalForAll: ApprovalForAll, + SRC5Event: SRC5Component::Event, + } + + #[derive(Copy, Drop, starknet::Event)] + struct Transfer { + from: ContractAddress, + to: ContractAddress, + token_id: u256 + } + + #[derive(Copy, Drop, starknet::Event)] + struct Approval { + owner: ContractAddress, + approved: ContractAddress, + token_id: u256 + } + + #[derive(Copy, Drop, starknet::Event)] + struct ApprovalForAll { + owner: ContractAddress, + operator: ContractAddress, + approved: bool + } + + mod Errors { + const INVALID_TOKEN_ID: felt252 = 'ERC721: invalid token ID'; + const INVALID_ACCOUNT: felt252 = 'ERC721: invalid account'; + const UNAUTHORIZED: felt252 = 'ERC721: unauthorized caller'; + const APPROVAL_TO_OWNER: felt252 = 'ERC721: approval to owner'; + const SELF_APPROVAL: felt252 = 'ERC721: self approval'; + const INVALID_RECEIVER: felt252 = 'ERC721: invalid receiver'; + const ALREADY_MINTED: felt252 = 'ERC721: token already minted'; + const WRONG_SENDER: felt252 = 'ERC721: wrong sender'; + } + + #[constructor] + fn constructor( + ref self: ContractState, + world: ContractAddress, + name: felt252, + symbol: felt252, + base_uri: felt252, + recipient: ContractAddress, + token_id: u256 + ) { + self._world.write(world); + self.initializer(name, symbol, base_uri); + self._mint(recipient, token_id); + } + + // + // External + // + + #[external(v0)] + impl ERC721MetadataImpl of interface::IERC721Metadata { + fn name(self: @ContractState) -> felt252 { + self.get_meta().name + } + + fn symbol(self: @ContractState) -> felt252 { + self.get_meta().symbol + } + + fn token_uri(self: @ContractState, token_id: u256) -> felt252 { + assert(self._exists(token_id), Errors::INVALID_TOKEN_ID); + // TODO : concat with id + self.get_uri(token_id) + } + } + + #[external(v0)] + impl ERC721MetadataCamelOnlyImpl of interface::IERC721MetadataCamelOnly { + fn tokenURI(self: @ContractState, tokenId: u256) -> felt252 { + self.token_uri(tokenId) + } + } + + #[external(v0)] + impl ERC721Impl of interface::IERC721 { + fn balance_of(self: @ContractState, account: ContractAddress) -> u256 { + assert(account.is_non_zero(), Errors::INVALID_ACCOUNT); + self.get_balance(account).amount + } + + fn owner_of(self: @ContractState, token_id: u256) -> ContractAddress { + self._owner_of(token_id) + } + + fn get_approved(self: @ContractState, token_id: u256) -> ContractAddress { + assert(self._exists(token_id), Errors::INVALID_TOKEN_ID); + self.get_token_approval(token_id).address + } + + fn is_approved_for_all( + self: @ContractState, owner: ContractAddress, operator: ContractAddress + ) -> bool { + self.get_operator_approval(owner, operator).approved + } + + fn approve(ref self: ContractState, to: ContractAddress, token_id: u256) { + let owner = self._owner_of(token_id); + + let caller = get_caller_address(); + assert( + owner == caller || ERC721Impl::is_approved_for_all(@self, owner, caller), + Errors::UNAUTHORIZED + ); + self._approve(to, token_id); + } + + fn set_approval_for_all( + ref self: ContractState, operator: ContractAddress, approved: bool + ) { + self._set_approval_for_all(get_caller_address(), operator, approved) + } + + fn transfer_from( + ref self: ContractState, from: ContractAddress, to: ContractAddress, token_id: u256 + ) { + assert( + self._is_approved_or_owner(get_caller_address(), token_id), Errors::UNAUTHORIZED + ); + self._transfer(from, to, token_id); + } + + fn safe_transfer_from( + ref self: ContractState, + from: ContractAddress, + to: ContractAddress, + token_id: u256, + data: Span + ) { + assert( + self._is_approved_or_owner(get_caller_address(), token_id), Errors::UNAUTHORIZED + ); + // TODO: move to real safe transfer when support of SRC6 is enabled + self.transfer_from(from, to, token_id); + } + } + + #[external(v0)] + impl ERC721CamelOnlyImpl of interface::IERC721CamelOnly { + fn balanceOf(self: @ContractState, account: ContractAddress) -> u256 { + ERC721Impl::balance_of(self, account) + } + + fn ownerOf(self: @ContractState, tokenId: u256) -> ContractAddress { + ERC721Impl::owner_of(self, tokenId) + } + + fn getApproved(self: @ContractState, tokenId: u256) -> ContractAddress { + ERC721Impl::get_approved(self, tokenId) + } + + fn isApprovedForAll( + self: @ContractState, owner: ContractAddress, operator: ContractAddress + ) -> bool { + ERC721Impl::is_approved_for_all(self, owner, operator) + } + + fn setApprovalForAll(ref self: ContractState, operator: ContractAddress, approved: bool) { + ERC721Impl::set_approval_for_all(ref self, operator, approved) + } + + fn transferFrom( + ref self: ContractState, from: ContractAddress, to: ContractAddress, tokenId: u256 + ) { + ERC721Impl::transfer_from(ref self, from, to, tokenId) + } + + fn safeTransferFrom( + ref self: ContractState, + from: ContractAddress, + to: ContractAddress, + tokenId: u256, + data: Span + ) { + ERC721Impl::safe_transfer_from(ref self, from, to, tokenId, data) + } + } + + // + // Internal + // + + #[generate_trait] + impl WorldInteractionsImpl of WorldInteractionsTrait { + fn world(self: @ContractState) -> IWorldDispatcher { + IWorldDispatcher { contract_address: self._world.read() } + } + + fn get_meta(self: @ContractState) -> ERC721Meta { + get!(self.world(), get_contract_address(), ERC721Meta) + } + + fn get_uri(self: @ContractState, token_id: u256) -> felt252 { + // TODO : concat with id when we have string type + self.get_meta().base_uri + } + + fn get_balance(self: @ContractState, account: ContractAddress) -> ERC721Balance { + get!(self.world(), (get_contract_address(), account), ERC721Balance) + } + + fn get_owner_of(self: @ContractState, token_id: u256) -> ERC721Owner { + get!(self.world(), (get_contract_address(), token_id), ERC721Owner) + } + + fn get_token_approval(self: @ContractState, token_id: u256) -> ERC721TokenApproval { + get!(self.world(), (get_contract_address(), token_id), ERC721TokenApproval) + } + + fn get_operator_approval( + self: @ContractState, owner: ContractAddress, operator: ContractAddress + ) -> ERC721OperatorApproval { + get!(self.world(), (get_contract_address(), owner, operator), ERC721OperatorApproval) + } + + fn set_token_approval( + ref self: ContractState, + owner: ContractAddress, + to: ContractAddress, + token_id: u256, + emit: bool + ) { + set!( + self.world(), + ERC721TokenApproval { token: get_contract_address(), token_id, address: to, } + ); + if emit { + self.emit_event(Approval { owner, approved: to, token_id }); + } + } + + fn set_operator_approval( + ref self: ContractState, + owner: ContractAddress, + operator: ContractAddress, + approved: bool + ) { + set!( + self.world(), + ERC721OperatorApproval { token: get_contract_address(), owner, operator, approved } + ); + self.emit_event(ApprovalForAll { owner, operator, approved }); + } + + fn set_balance(ref self: ContractState, account: ContractAddress, amount: u256) { + set!(self.world(), ERC721Balance { token: get_contract_address(), account, amount }); + } + + fn set_owner(ref self: ContractState, token_id: u256, address: ContractAddress) { + set!(self.world(), ERC721Owner { token: get_contract_address(), token_id, address }); + } + + fn emit_event< + S, impl IntoImp: traits::Into, impl SDrop: Drop, impl SCopy: Copy + >( + ref self: ContractState, event: S + ) { + self.emit(event); + emit!(self.world(), event); + } + } + + #[generate_trait] + impl InternalImpl of InternalTrait { + fn initializer(ref self: ContractState, name: felt252, symbol: felt252, base_uri: felt252) { + let meta = ERC721Meta { token: get_contract_address(), name, symbol, base_uri }; + set!(self.world(), (meta)); + self.src5.register_interface(interface::IERC721_ID); + self.src5.register_interface(interface::IERC721_METADATA_ID); + } + + fn _owner_of(self: @ContractState, token_id: u256) -> ContractAddress { + let owner = self.get_owner_of(token_id).address; + match owner.is_zero() { + bool::False(()) => owner, + bool::True(()) => panic_with_felt252(Errors::INVALID_TOKEN_ID) + } + } + + fn _exists(self: @ContractState, token_id: u256) -> bool { + let owner = self.get_owner_of(token_id).address; + owner.is_non_zero() + } + + fn _is_approved_or_owner( + self: @ContractState, spender: ContractAddress, token_id: u256 + ) -> bool { + let owner = self._owner_of(token_id); + let is_approved_for_all = ERC721Impl::is_approved_for_all(self, owner, spender); + owner == spender + || is_approved_for_all + || spender == ERC721Impl::get_approved(self, token_id) + } + + fn _approve(ref self: ContractState, to: ContractAddress, token_id: u256) { + let owner = self._owner_of(token_id); + assert(owner != to, Errors::APPROVAL_TO_OWNER); + + self.set_token_approval(owner, to, token_id, true); + } + + fn _set_approval_for_all( + ref self: ContractState, + owner: ContractAddress, + operator: ContractAddress, + approved: bool + ) { + assert(owner != operator, Errors::SELF_APPROVAL); + self.set_operator_approval(owner, operator, approved); + } + + fn _mint(ref self: ContractState, to: ContractAddress, token_id: u256) { + assert(!to.is_zero(), Errors::INVALID_RECEIVER); + assert(!self._exists(token_id), Errors::ALREADY_MINTED); + + self.set_balance(to, self.get_balance(to).amount + 1); + self.set_owner(token_id, to); + + self.emit_event(Transfer { from: Zeroable::zero(), to, token_id }); + } + + fn _transfer( + ref self: ContractState, from: ContractAddress, to: ContractAddress, token_id: u256 + ) { + assert(!to.is_zero(), Errors::INVALID_RECEIVER); + let owner = self._owner_of(token_id); + assert(from == owner, Errors::WRONG_SENDER); + + // Implicit clear approvals, no need to emit an event + self.set_token_approval(owner, Zeroable::zero(), token_id, false); + + self.set_balance(from, self.get_balance(from).amount - 1); + self.set_balance(to, self.get_balance(to).amount + 1); + self.set_owner(token_id, to); + + self.emit_event(Transfer { from, to, token_id }); + } + + fn _burn(ref self: ContractState, token_id: u256) { + let owner = self._owner_of(token_id); + + // Implicit clear approvals, no need to emit an event + self.set_token_approval(owner, Zeroable::zero(), token_id, false); + + self.set_balance(owner, self.get_balance(owner).amount - 1); + self.set_owner(token_id, Zeroable::zero()); + + self.emit_event(Transfer { from: owner, to: Zeroable::zero(), token_id }); + } + } +} diff --git a/presets/src/erc721/models.cairo b/presets/src/erc721/models.cairo new file mode 100644 index 00000000..eec7ec9c --- /dev/null +++ b/presets/src/erc721/models.cairo @@ -0,0 +1,50 @@ +// Starknet imports + +use starknet::ContractAddress; + +#[derive(Model, Copy, Drop, Serde)] +struct ERC721Meta { + #[key] + token: ContractAddress, + name: felt252, + symbol: felt252, + base_uri: felt252, +} + +#[derive(Model, Copy, Drop, Serde)] +struct ERC721OperatorApproval { + #[key] + token: ContractAddress, + #[key] + owner: ContractAddress, + #[key] + operator: ContractAddress, + approved: bool +} + +#[derive(Model, Copy, Drop, Serde)] +struct ERC721Owner { + #[key] + token: ContractAddress, + #[key] + token_id: u256, + address: ContractAddress +} + +#[derive(Model, Copy, Drop, Serde)] +struct ERC721Balance { + #[key] + token: ContractAddress, + #[key] + account: ContractAddress, + amount: u256, +} + +#[derive(Model, Copy, Drop, Serde)] +struct ERC721TokenApproval { + #[key] + token: ContractAddress, + #[key] + token_id: u256, + address: ContractAddress, +} \ No newline at end of file diff --git a/presets/src/erc721/tests.cairo b/presets/src/erc721/tests.cairo new file mode 100644 index 00000000..cdeef6be --- /dev/null +++ b/presets/src/erc721/tests.cairo @@ -0,0 +1,1464 @@ +// Core imports + +use integer::u256; +use integer::u256_from_felt252; +use zeroable::Zeroable; +use debug::PrintTrait; + +// Starknet imports + +use starknet::ContractAddress; +use starknet::contract_address_const; +use starknet::testing; + +// Dojo imports + +use dojo::test_utils::spawn_test_world; +use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait}; + +// External imports + +// Internal imports + +use presets::tests::utils; +use presets::tests::constants::{ + ZERO, OWNER, SPENDER, RECIPIENT, OPERATOR, OTHER, NAME, SYMBOL, URI, TOKEN_ID +}; +use presets::erc721::ERC721::ERC721Impl; +use presets::erc721::ERC721::ERC721CamelOnlyImpl; +use presets::erc721::ERC721::ERC721MetadataImpl; +use presets::erc721::ERC721::InternalImpl; +use presets::erc721::ERC721::WorldInteractionsImpl; +use presets::erc721::ERC721::{Approval, ApprovalForAll, Transfer}; +use presets::erc721::ERC721; +use presets::erc721::models::{ + ERC721Meta, erc_721_meta, ERC721OperatorApproval, erc_721_operator_approval, ERC721Owner, + erc_721_owner, ERC721Balance, erc_721_balance, ERC721TokenApproval, erc_721_token_approval +}; +use presets::erc721::ERC721::_worldContractMemberStateTrait; + +// +// Setup +// + +fn STATE() -> (IWorldDispatcher, ERC721::ContractState) { + let world = spawn_test_world( + array![ + erc_721_meta::TEST_CLASS_HASH, + erc_721_operator_approval::TEST_CLASS_HASH, + erc_721_owner::TEST_CLASS_HASH, + erc_721_balance::TEST_CLASS_HASH, + erc_721_token_approval::TEST_CLASS_HASH, + ] + ); + let mut state = ERC721::contract_state_for_testing(); + state._world.write(world.contract_address); + (world, state) +} + +fn setup() -> ERC721::ContractState { + let (world, mut state) = STATE(); + ERC721::constructor(ref state, world.contract_address, NAME, SYMBOL, URI, OWNER(), TOKEN_ID); + utils::drop_event(ZERO()); + state +} + +// fn setup_receiver() -> ContractAddress { +// utils::deploy(ERC721Receiver::TEST_CLASS_HASH, array![]) +// } + +// fn setup_camel_receiver() -> ContractAddress { +// utils::deploy(CamelERC721ReceiverMock::TEST_CLASS_HASH, array![]) +// } + +// fn setup_account() -> ContractAddress { +// let mut calldata = array![PUBKEY]; +// utils::deploy(Account::TEST_CLASS_HASH, calldata) +// } + +// fn setup_camel_account() -> ContractAddress { +// let mut calldata = array![PUBKEY]; +// utils::deploy(CamelAccountMock::TEST_CLASS_HASH, calldata) +// } + +// +// initializer & constructor +// + +#[test] +#[available_gas(20000000)] +fn test_constructor() { + let (world, mut state) = STATE(); + ERC721::constructor(ref state, world.contract_address, NAME, SYMBOL, URI, OWNER(), TOKEN_ID); + + assert(ERC721MetadataImpl::name(@state) == NAME, 'Name should be NAME'); + assert(ERC721MetadataImpl::symbol(@state) == SYMBOL, 'Symbol should be SYMBOL'); + assert(ERC721Impl::balance_of(@state, OWNER()) == 1, 'Balance should be one'); + assert(ERC721Impl::owner_of(@state, TOKEN_ID) == OWNER(), 'OWNER should be owner'); +// assert( +// SRC5Impl::supports_interface(@state, erc721::interface::IERC721_ID), 'Missing interface ID' +// ); +// assert( +// SRC5Impl::supports_interface(@state, erc721::interface::IERC721_METADATA_ID), +// 'missing interface ID' +// ); +// assert( +// SRC5Impl::supports_interface(@state, introspection::interface::ISRC5_ID), +// 'missing interface ID' +// ); +} + +#[test] +#[available_gas(10000000)] +fn test_initializer() { + let (world, mut state) = STATE(); + InternalImpl::initializer(ref state, NAME, SYMBOL, URI); + + assert(ERC721MetadataImpl::name(@state) == NAME, 'Name should be NAME'); + assert(ERC721MetadataImpl::symbol(@state) == SYMBOL, 'Symbol should be SYMBOL'); + + assert(ERC721Impl::balance_of(@state, OWNER()) == 0, 'Balance should be zero'); +// assert( +// SRC5Impl::supports_interface(@state, erc721::interface::IERC721_ID), 'Missing interface ID' +// ); +// assert( +// SRC5Impl::supports_interface(@state, erc721::interface::IERC721_METADATA_ID), +// 'missing interface ID' +// ); +// assert( +// SRC5Impl::supports_interface(@state, introspection::interface::ISRC5_ID), +// 'missing interface ID' +// ); +} + + +// +// Getters +// + +#[test] +#[available_gas(20000000)] +fn test_balance_of() { + let state = setup(); + assert(ERC721Impl::balance_of(@state, OWNER()) == 1, 'Should return balance'); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC721: invalid account',))] +fn test_balance_of_zero() { + let state = setup(); + ERC721Impl::balance_of(@state, ZERO()); +} + +#[test] +#[available_gas(20000000)] +fn test_owner_of() { + let state = setup(); + assert(ERC721Impl::owner_of(@state, TOKEN_ID) == OWNER(), 'Should return owner'); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC721: invalid token ID',))] +fn test_owner_of_non_minted() { + let state = setup(); + ERC721Impl::owner_of(@state, u256_from_felt252(7)); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC721: invalid token ID',))] +fn test_token_uri_non_minted() { + let state = setup(); + ERC721MetadataImpl::token_uri(@state, u256_from_felt252(7)); +} + +#[test] +#[available_gas(20000000)] +fn test_get_approved() { + let mut state = setup(); + let spender = SPENDER(); + let token_id = TOKEN_ID; + + assert(ERC721Impl::get_approved(@state, token_id) == ZERO(), 'Should return non-approval'); + InternalImpl::_approve(ref state, spender, token_id); + assert(ERC721Impl::get_approved(@state, token_id) == spender, 'Should return approval'); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC721: invalid token ID',))] +fn test_get_approved_nonexistent() { + let mut state = setup(); + ERC721Impl::get_approved(@state, u256_from_felt252(7)); +} + +#[test] +#[available_gas(20000000)] +fn test__exists() { + let (world, mut state) = STATE(); + let token_id = TOKEN_ID; + + assert(!InternalImpl::_exists(@state, token_id), 'Token should not exist'); + assert( + WorldInteractionsImpl::get_owner_of(@state, token_id).address == ZERO(), 'Invalid owner' + ); + + InternalImpl::_mint(ref state, RECIPIENT(), token_id); + + assert(InternalImpl::_exists(@state, token_id), 'Token should exist'); + assert( + WorldInteractionsImpl::get_owner_of(@state, token_id).address == RECIPIENT(), + 'Invalid owner' + ); + + InternalImpl::_burn(ref state, token_id); + + assert(!InternalImpl::_exists(@state, token_id), 'Token should not exist'); + assert( + WorldInteractionsImpl::get_owner_of(@state, token_id).address == ZERO(), 'Invalid owner' + ); +} + + +// +// approve & _approve +// + +#[test] +#[available_gas(20000000)] +fn test_approve_from_owner() { + let mut state = setup(); + + testing::set_caller_address(OWNER()); + ERC721Impl::approve(ref state, SPENDER(), TOKEN_ID); + assert_event_approval(OWNER(), SPENDER(), TOKEN_ID); + + assert( + ERC721Impl::get_approved(@state, TOKEN_ID) == SPENDER(), 'Spender not approved correctly' + ); +} + +#[test] +#[available_gas(20000000)] +fn test_approve_from_operator() { + let mut state = setup(); + + testing::set_caller_address(OWNER()); + ERC721Impl::set_approval_for_all(ref state, OPERATOR(), true); + utils::drop_event(ZERO()); + + testing::set_caller_address(OPERATOR()); + ERC721Impl::approve(ref state, SPENDER(), TOKEN_ID); + assert_event_approval(OWNER(), SPENDER(), TOKEN_ID); + + assert( + ERC721Impl::get_approved(@state, TOKEN_ID) == SPENDER(), 'Spender not approved correctly' + ); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC721: unauthorized caller',))] +fn test_approve_from_unauthorized() { + let mut state = setup(); + + testing::set_caller_address(OTHER()); + ERC721Impl::approve(ref state, SPENDER(), TOKEN_ID); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC721: approval to owner',))] +fn test_approve_to_owner() { + let mut state = setup(); + + testing::set_caller_address(OWNER()); + ERC721Impl::approve(ref state, OWNER(), TOKEN_ID); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC721: invalid token ID',))] +fn test_approve_nonexistent() { + // let mut state = STATE(); + let (world, mut state) = STATE(); + ERC721Impl::approve(ref state, SPENDER(), TOKEN_ID); +} + +#[test] +#[available_gas(20000000)] +fn test__approve() { + let mut state = setup(); + InternalImpl::_approve(ref state, SPENDER(), TOKEN_ID); + assert_event_approval(OWNER(), SPENDER(), TOKEN_ID); + + assert( + ERC721Impl::get_approved(@state, TOKEN_ID) == SPENDER(), 'Spender not approved correctly' + ); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC721: approval to owner',))] +fn test__approve_to_owner() { + let mut state = setup(); + InternalImpl::_approve(ref state, OWNER(), TOKEN_ID); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC721: invalid token ID',))] +fn test__approve_nonexistent() { + //let mut state = STATE(); + let (world, mut state) = STATE(); + InternalImpl::_approve(ref state, SPENDER(), TOKEN_ID); +} + +// +// set_approval_for_all & _set_approval_for_all +// + +#[test] +#[available_gas(20000000)] +fn test_set_approval_for_all() { + //let mut state = STATE(); + let (world, mut state) = STATE(); + testing::set_caller_address(OWNER()); + + assert(!ERC721Impl::is_approved_for_all(@state, OWNER(), OPERATOR()), 'Invalid default value'); + + ERC721Impl::set_approval_for_all(ref state, OPERATOR(), true); + assert_event_approval_for_all(OWNER(), OPERATOR(), true); + + assert( + ERC721Impl::is_approved_for_all(@state, OWNER(), OPERATOR()), + 'Operator not approved correctly' + ); + + ERC721Impl::set_approval_for_all(ref state, OPERATOR(), false); + assert_event_approval_for_all(OWNER(), OPERATOR(), false); + + assert( + !ERC721Impl::is_approved_for_all(@state, OWNER(), OPERATOR()), + 'Approval not revoked correctly' + ); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC721: self approval',))] +fn test_set_approval_for_all_owner_equal_operator_true() { + //let mut state = STATE(); + let (world, mut state) = STATE(); + testing::set_caller_address(OWNER()); + ERC721Impl::set_approval_for_all(ref state, OWNER(), true); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC721: self approval',))] +fn test_set_approval_for_all_owner_equal_operator_false() { + //let mut state = STATE(); + let (world, mut state) = STATE(); + testing::set_caller_address(OWNER()); + ERC721Impl::set_approval_for_all(ref state, OWNER(), false); +} + +#[test] +#[available_gas(20000000)] +fn test__set_approval_for_all() { + //let mut state = STATE(); + let (world, mut state) = STATE(); + assert(!ERC721Impl::is_approved_for_all(@state, OWNER(), OPERATOR()), 'Invalid default value'); + + InternalImpl::_set_approval_for_all(ref state, OWNER(), OPERATOR(), true); + assert_event_approval_for_all(OWNER(), OPERATOR(), true); + + assert( + ERC721Impl::is_approved_for_all(@state, OWNER(), OPERATOR()), + 'Operator not approved correctly' + ); + + InternalImpl::_set_approval_for_all(ref state, OWNER(), OPERATOR(), false); + assert_event_approval_for_all(OWNER(), OPERATOR(), false); + + assert( + !ERC721Impl::is_approved_for_all(@state, OWNER(), OPERATOR()), + 'Operator not approved correctly' + ); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC721: self approval',))] +fn test__set_approval_for_all_owner_equal_operator_true() { + //let mut state = STATE(); + let (world, mut state) = STATE(); + InternalImpl::_set_approval_for_all(ref state, OWNER(), OWNER(), true); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC721: self approval',))] +fn test__set_approval_for_all_owner_equal_operator_false() { + // let mut state = STATE(); + let (world, mut state) = STATE(); + InternalImpl::_set_approval_for_all(ref state, OWNER(), OWNER(), false); +} + + +// +// transfer_from & transferFrom +// + +#[test] +#[available_gas(60000000)] +fn test_transfer_from_owner() { + let mut state = setup(); + let token_id = TOKEN_ID; + let owner = OWNER(); + let recipient = RECIPIENT(); + // set approval to check reset + InternalImpl::_approve(ref state, OTHER(), token_id); + utils::drop_event(ZERO()); + + assert_state_before_transfer(@state, owner, recipient, token_id); + assert(ERC721Impl::get_approved(@state, token_id) == OTHER(), 'Approval not implicitly reset'); + + testing::set_caller_address(owner); + ERC721Impl::transfer_from(ref state, owner, recipient, token_id); + assert_event_transfer(owner, recipient, token_id); + + assert_state_after_transfer(@state, owner, recipient, token_id); +} + +#[test] +#[available_gas(50000000)] +fn test_transferFrom_owner() { + let mut state = setup(); + let token_id = TOKEN_ID; + let owner = OWNER(); + let recipient = RECIPIENT(); + // set approval to check reset + InternalImpl::_approve(ref state, OTHER(), token_id); + utils::drop_event(ZERO()); + + assert_state_before_transfer(@state, owner, recipient, token_id); + assert(ERC721Impl::get_approved(@state, token_id) == OTHER(), 'Approval not implicitly reset'); + + testing::set_caller_address(owner); + ERC721CamelOnlyImpl::transferFrom(ref state, owner, recipient, token_id); + assert_event_transfer(owner, recipient, token_id); + + assert_state_after_transfer(@state, owner, recipient, token_id); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC721: invalid token ID',))] +fn test_transfer_from_nonexistent() { + //let mut state = STATE(); + let (world, mut state) = STATE(); + ERC721Impl::transfer_from(ref state, ZERO(), RECIPIENT(), TOKEN_ID); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC721: invalid token ID',))] +fn test_transferFrom_nonexistent() { + //let mut state = STATE(); + let (world, mut state) = STATE(); + ERC721CamelOnlyImpl::transferFrom(ref state, ZERO(), RECIPIENT(), TOKEN_ID); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC721: invalid receiver',))] +fn test_transfer_from_to_zero() { + let mut state = setup(); + testing::set_caller_address(OWNER()); + ERC721Impl::transfer_from(ref state, OWNER(), ZERO(), TOKEN_ID); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC721: invalid receiver',))] +fn test_transferFrom_to_zero() { + let mut state = setup(); + + testing::set_caller_address(OWNER()); + ERC721CamelOnlyImpl::transferFrom(ref state, OWNER(), ZERO(), TOKEN_ID); +} + +#[test] +#[available_gas(50000000)] +fn test_transfer_from_to_owner() { + let mut state = setup(); + + assert(ERC721Impl::owner_of(@state, TOKEN_ID) == OWNER(), 'Ownership before'); + assert(ERC721Impl::balance_of(@state, OWNER()) == 1, 'Balance of owner before'); + + testing::set_caller_address(OWNER()); + ERC721Impl::transfer_from(ref state, OWNER(), OWNER(), TOKEN_ID); + assert_event_transfer(OWNER(), OWNER(), TOKEN_ID); + + assert(ERC721Impl::owner_of(@state, TOKEN_ID) == OWNER(), 'Ownership after'); + assert(ERC721Impl::balance_of(@state, OWNER()) == 1, 'Balance of owner after'); +} + +#[test] +#[available_gas(50000000)] +fn test_transferFrom_to_owner() { + let mut state = setup(); + + assert(ERC721Impl::owner_of(@state, TOKEN_ID) == OWNER(), 'Ownership before'); + assert(ERC721Impl::balance_of(@state, OWNER()) == 1, 'Balance of owner before'); + + testing::set_caller_address(OWNER()); + ERC721CamelOnlyImpl::transferFrom(ref state, OWNER(), OWNER(), TOKEN_ID); + assert_event_transfer( OWNER(), OWNER(), TOKEN_ID); + + assert(ERC721Impl::owner_of(@state, TOKEN_ID) == OWNER(), 'Ownership after'); + assert(ERC721Impl::balance_of(@state, OWNER()) == 1, 'Balance of owner after'); +} + +#[test] +#[available_gas(50000000)] +fn test_transfer_from_approved() { + let mut state = setup(); + let token_id = TOKEN_ID; + let owner = OWNER(); + let recipient = RECIPIENT(); + assert_state_before_transfer(@state, owner, recipient, token_id); + + testing::set_caller_address(owner); + ERC721Impl::approve(ref state, OPERATOR(), token_id); + utils::drop_event(ZERO()); + + testing::set_caller_address(OPERATOR()); + ERC721Impl::transfer_from(ref state, owner, recipient, token_id); + assert_event_transfer(owner, recipient, token_id); + + assert_state_after_transfer(@state, owner, recipient, token_id); +} + +#[test] +#[available_gas(50000000)] +fn test_transferFrom_approved() { + let mut state = setup(); + let token_id = TOKEN_ID; + let owner = OWNER(); + let recipient = RECIPIENT(); + assert_state_before_transfer(@state,owner, recipient, token_id); + + testing::set_caller_address(owner); + ERC721Impl::approve(ref state, OPERATOR(), token_id); + utils::drop_event(ZERO()); + + testing::set_caller_address(OPERATOR()); + ERC721CamelOnlyImpl::transferFrom(ref state, owner, recipient, token_id); + assert_event_transfer(owner, recipient, token_id); + + assert_state_after_transfer(@state,owner, recipient, token_id); +} + +#[test] +#[available_gas(50000000)] +fn test_transfer_from_approved_for_all() { + let mut state = setup(); + let token_id = TOKEN_ID; + let owner = OWNER(); + let recipient = RECIPIENT(); + + assert_state_before_transfer(@state, owner, recipient, token_id); + + testing::set_caller_address(owner); + ERC721Impl::set_approval_for_all(ref state, OPERATOR(), true); + utils::drop_event(ZERO()); + + testing::set_caller_address(OPERATOR()); + ERC721Impl::transfer_from(ref state, owner, recipient, token_id); + assert_event_transfer(owner, recipient, token_id); + + assert_state_after_transfer(@state, owner, recipient, token_id); +} + +#[test] +#[available_gas(50000000)] +fn test_transferFrom_approved_for_all() { + let mut state = setup(); + let token_id = TOKEN_ID; + let owner = OWNER(); + let recipient = RECIPIENT(); + + assert_state_before_transfer(@state,owner, recipient, token_id); + + testing::set_caller_address(owner); + ERC721Impl::set_approval_for_all(ref state, OPERATOR(), true); + utils::drop_event(ZERO()); + + testing::set_caller_address(OPERATOR()); + ERC721CamelOnlyImpl::transferFrom(ref state, owner, recipient, token_id); + assert_event_transfer(owner, recipient, token_id); + + assert_state_after_transfer(@state,owner, recipient, token_id); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC721: unauthorized caller',))] +fn test_transfer_from_unauthorized() { + let mut state = setup(); + testing::set_caller_address(OTHER()); + ERC721Impl::transfer_from(ref state, OWNER(), RECIPIENT(), TOKEN_ID); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC721: unauthorized caller',))] +fn test_transferFrom_unauthorized() { + let mut state = setup(); + testing::set_caller_address(OTHER()); + ERC721CamelOnlyImpl::transferFrom(ref state, OWNER(), RECIPIENT(), TOKEN_ID); +} + +// // +// // safe_transfer_from & safeTransferFrom +// // + +// #[test] +// #[available_gas(20000000)] +// fn test_safe_transfer_from_to_account() { +// let mut state = setup(); +// let account = setup_account(); +// let token_id = TOKEN_ID; +// let owner = OWNER(); + +// assert_state_before_transfer(@state,owner, account, token_id); + +// testing::set_caller_address(owner); +// ERC721Impl::safe_transfer_from(ref state, owner, account, token_id, DATA(true)); +// assert_event_transfer(owner, account, token_id); + +// assert_state_after_transfer(@state,owner, account, token_id); +// } + +// #[test] +// #[available_gas(20000000)] +// fn test_safeTransferFrom_to_account() { +// let mut state = setup(); +// let account = setup_account(); +// let token_id = TOKEN_ID; +// let owner = OWNER(); + +// assert_state_before_transfer(@state,owner, account, token_id); + +// testing::set_caller_address(owner); +// ERC721CamelOnlyImpl::safeTransferFrom(ref state, owner, account, token_id, DATA(true)); +// assert_event_transfer(owner, account, token_id); + +// assert_state_after_transfer(@state,owner, account, token_id); +// } + +// #[test] +// #[available_gas(20000000)] +// fn test_safe_transfer_from_to_account_camel() { +// let mut state = setup(); +// let account = setup_camel_account(); +// let token_id = TOKEN_ID; +// let owner = OWNER(); + +// assert_state_before_transfer(@state,owner, account, token_id); + +// testing::set_caller_address(owner); +// ERC721Impl::safe_transfer_from(ref state, owner, account, token_id, DATA(true)); +// assert_event_transfer(owner, account, token_id); + +// assert_state_after_transfer(@state,owner, account, token_id); +// } + +// #[test] +// #[available_gas(20000000)] +// fn test_safeTransferFrom_to_account_camel() { +// let mut state = setup(); +// let account = setup_camel_account(); +// let token_id = TOKEN_ID; +// let owner = OWNER(); + +// assert_state_before_transfer(@state,owner, account, token_id); + +// testing::set_caller_address(owner); +// ERC721CamelOnlyImpl::safeTransferFrom(ref state, owner, account, token_id, DATA(true)); +// assert_event_transfer(owner, account, token_id); + +// assert_state_after_transfer(@state,owner, account, token_id); +// } + +// #[test] +// #[available_gas(20000000)] +// fn test_safe_transfer_from_to_receiver() { +// let mut state = setup(); +// let receiver = setup_receiver(); +// let token_id = TOKEN_ID; +// let owner = OWNER(); + +// assert_state_before_transfer(@state,owner, receiver, token_id); + +// testing::set_caller_address(owner); +// ERC721Impl::safe_transfer_from(ref state, owner, receiver, token_id, DATA(true)); +// assert_event_transfer(owner, receiver, token_id); + +// assert_state_after_transfer(@state,owner, receiver, token_id); +// } + +// #[test] +// #[available_gas(20000000)] +// fn test_safeTransferFrom_to_receiver() { +// let mut state = setup(); +// let receiver = setup_receiver(); +// let token_id = TOKEN_ID; +// let owner = OWNER(); + +// assert_state_before_transfer(@state,owner, receiver, token_id); + +// testing::set_caller_address(owner); +// ERC721CamelOnlyImpl::safeTransferFrom(ref state, owner, receiver, token_id, DATA(true)); +// assert_event_transfer(owner, receiver, token_id); + +// assert_state_after_transfer(@state,owner, receiver, token_id); +// } + +// #[test] +// #[available_gas(20000000)] +// fn test_safe_transfer_from_to_receiver_camel() { +// let mut state = setup(); +// let receiver = setup_camel_receiver(); +// let token_id = TOKEN_ID; +// let owner = OWNER(); + +// assert_state_before_transfer(@state,owner, receiver, token_id); + +// testing::set_caller_address(owner); +// ERC721Impl::safe_transfer_from(ref state, owner, receiver, token_id, DATA(true)); +// assert_event_transfer(owner, receiver, token_id); + +// assert_state_after_transfer(@state,owner, receiver, token_id); +// } + +// #[test] +// #[available_gas(20000000)] +// fn test_safeTransferFrom_to_receiver_camel() { +// let mut state = setup(); +// let receiver = setup_camel_receiver(); +// let token_id = TOKEN_ID; +// let owner = OWNER(); + +// assert_state_before_transfer(@state,owner, receiver, token_id); + +// testing::set_caller_address(owner); +// ERC721CamelOnlyImpl::safeTransferFrom(ref state, owner, receiver, token_id, DATA(true)); +// assert_event_transfer(owner, receiver, token_id); + +// assert_state_after_transfer(@state,owner, receiver, token_id); +// } + +// #[test] +// #[available_gas(20000000)] +// #[should_panic(expected: ('ERC721: safe transfer failed',))] +// fn test_safe_transfer_from_to_receiver_failure() { +// let mut state = setup(); +// let receiver = setup_receiver(); +// let token_id = TOKEN_ID; +// let owner = OWNER(); + +// testing::set_caller_address(owner); +// ERC721Impl::safe_transfer_from(ref state, owner, receiver, token_id, DATA(false)); +// } + +// #[test] +// #[available_gas(20000000)] +// #[should_panic(expected: ('ERC721: safe transfer failed',))] +// fn test_safeTransferFrom_to_receiver_failure() { +// let mut state = setup(); +// let receiver = setup_receiver(); +// let token_id = TOKEN_ID; +// let owner = OWNER(); + +// testing::set_caller_address(owner); +// ERC721CamelOnlyImpl::safeTransferFrom(ref state, owner, receiver, token_id, DATA(false)); +// } + +// #[test] +// #[available_gas(20000000)] +// #[should_panic(expected: ('ERC721: safe transfer failed',))] +// fn test_safe_transfer_from_to_receiver_failure_camel() { +// let mut state = setup(); +// let receiver = setup_camel_receiver(); +// let token_id = TOKEN_ID; +// let owner = OWNER(); + +// testing::set_caller_address(owner); +// ERC721Impl::safe_transfer_from(ref state, owner, receiver, token_id, DATA(false)); +// } + +// #[test] +// #[available_gas(20000000)] +// #[should_panic(expected: ('ERC721: safe transfer failed',))] +// fn test_safeTransferFrom_to_receiver_failure_camel() { +// let mut state = setup(); +// let receiver = setup_camel_receiver(); +// let token_id = TOKEN_ID; +// let owner = OWNER(); + +// testing::set_caller_address(owner); +// ERC721CamelOnlyImpl::safeTransferFrom(ref state, owner, receiver, token_id, DATA(false)); +// } + +// #[test] +// #[available_gas(20000000)] +// #[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] +// fn test_safe_transfer_from_to_non_receiver() { +// let mut state = setup(); +// let recipient = utils::deploy(NonImplementingMock::TEST_CLASS_HASH, array![]); +// let token_id = TOKEN_ID; +// let owner = OWNER(); + +// testing::set_caller_address(owner); +// ERC721Impl::safe_transfer_from(ref state, owner, recipient, token_id, DATA(true)); +// } + +// #[test] +// #[available_gas(20000000)] +// #[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] +// fn test_safeTransferFrom_to_non_receiver() { +// let mut state = setup(); +// let recipient = utils::deploy(NonImplementingMock::TEST_CLASS_HASH, array![]); +// let token_id = TOKEN_ID; +// let owner = OWNER(); + +// testing::set_caller_address(owner); +// ERC721CamelOnlyImpl::safeTransferFrom(ref state, owner, recipient, token_id, DATA(true)); +// } + +// #[test] +// #[available_gas(20000000)] +// #[should_panic(expected: ('ERC721: invalid token ID',))] +// fn test_safe_transfer_from_nonexistent() { +// let mut state = STATE(); +// ERC721Impl::safe_transfer_from(ref state, ZERO(), RECIPIENT(), TOKEN_ID, DATA(true)); +// } + +// #[test] +// #[available_gas(20000000)] +// #[should_panic(expected: ('ERC721: invalid token ID',))] +// fn test_safeTransferFrom_nonexistent() { +// let mut state = STATE(); +// ERC721CamelOnlyImpl::safeTransferFrom(ref state, ZERO(), RECIPIENT(), TOKEN_ID, DATA(true)); +// } + +// #[test] +// #[available_gas(20000000)] +// #[should_panic(expected: ('ERC721: invalid receiver',))] +// fn test_safe_transfer_from_to_zero() { +// let mut state = setup(); +// testing::set_caller_address(OWNER()); +// ERC721Impl::safe_transfer_from(ref state, OWNER(), ZERO(), TOKEN_ID, DATA(true)); +// } + +// #[test] +// #[available_gas(20000000)] +// #[should_panic(expected: ('ERC721: invalid receiver',))] +// fn test_safeTransferFrom_to_zero() { +// let mut state = setup(); +// testing::set_caller_address(OWNER()); +// ERC721CamelOnlyImpl::safeTransferFrom(ref state, OWNER(), ZERO(), TOKEN_ID, DATA(true)); +// } + +// #[test] +// #[available_gas(20000000)] +// fn test_safe_transfer_from_to_owner() { +// let mut state = STATE(); +// let token_id = TOKEN_ID; +// let owner = setup_receiver(); +// InternalImpl::initializer(ref state, NAME, SYMBOL); +// InternalImpl::_mint(ref state, owner, token_id); +// utils::drop_event(ZERO()); + +// assert(ERC721Impl::owner_of(@state, token_id) == owner, 'Ownership before'); +// assert(ERC721Impl::balance_of(@state, owner) == 1, 'Balance of owner before'); + +// testing::set_caller_address(owner); +// ERC721Impl::safe_transfer_from(ref state, owner, owner, token_id, DATA(true)); +// assert_event_transfer(owner, owner, token_id); + +// assert(ERC721Impl::owner_of(@state, token_id) == owner, 'Ownership after'); +// assert(ERC721Impl::balance_of(@state, owner) == 1, 'Balance of owner after'); +// } + +// #[test] +// #[available_gas(20000000)] +// fn test_safeTransferFrom_to_owner() { +// let mut state = STATE(); +// let token_id = TOKEN_ID; +// let owner = setup_receiver(); +// InternalImpl::initializer(ref state, NAME, SYMBOL); +// InternalImpl::_mint(ref state, owner, token_id); +// utils::drop_event(ZERO()); + +// assert(ERC721Impl::owner_of(@state, token_id) == owner, 'Ownership before'); +// assert(ERC721Impl::balance_of(@state, owner) == 1, 'Balance of owner before'); + +// testing::set_caller_address(owner); +// ERC721CamelOnlyImpl::safeTransferFrom(ref state, owner, owner, token_id, DATA(true)); +// assert_event_transfer(owner, owner, token_id); + +// assert(ERC721Impl::owner_of(@state, token_id) == owner, 'Ownership after'); +// assert(ERC721Impl::balance_of(@state, owner) == 1, 'Balance of owner after'); +// } + +// #[test] +// #[available_gas(20000000)] +// fn test_safe_transfer_from_to_owner_camel() { +// let mut state = STATE(); +// let token_id = TOKEN_ID; +// let owner = setup_camel_receiver(); +// InternalImpl::initializer(ref state, NAME, SYMBOL); +// InternalImpl::_mint(ref state, owner, token_id); +// utils::drop_event(ZERO()); + +// assert(ERC721Impl::owner_of(@state, token_id) == owner, 'Ownership before'); +// assert(ERC721Impl::balance_of(@state, owner) == 1, 'Balance of owner before'); + +// testing::set_caller_address(owner); +// ERC721Impl::safe_transfer_from(ref state, owner, owner, token_id, DATA(true)); +// assert_event_transfer(owner, owner, token_id); + +// assert(ERC721Impl::owner_of(@state, token_id) == owner, 'Ownership after'); +// assert(ERC721Impl::balance_of(@state, owner) == 1, 'Balance of owner after'); +// } + +// #[test] +// #[available_gas(20000000)] +// fn test_safeTransferFrom_to_owner_camel() { +// let mut state = STATE(); +// let token_id = TOKEN_ID; +// let owner = setup_camel_receiver(); +// InternalImpl::initializer(ref state, NAME, SYMBOL); +// InternalImpl::_mint(ref state, owner, token_id); +// utils::drop_event(ZERO()); + +// assert(ERC721Impl::owner_of(@state, token_id) == owner, 'Ownership before'); +// assert(ERC721Impl::balance_of(@state, owner) == 1, 'Balance of owner before'); + +// testing::set_caller_address(owner); +// ERC721CamelOnlyImpl::safeTransferFrom(ref state, owner, owner, token_id, DATA(true)); +// assert_event_transfer(owner, owner, token_id); + +// assert(ERC721Impl::owner_of(@state, token_id) == owner, 'Ownership after'); +// assert(ERC721Impl::balance_of(@state, owner) == 1, 'Balance of owner after'); +// } + +// #[test] +// #[available_gas(20000000)] +// fn test_safe_transfer_from_approved() { +// let mut state = setup(); +// let receiver = setup_receiver(); +// let token_id = TOKEN_ID; +// let owner = OWNER(); + +// assert_state_before_transfer(@state,owner, receiver, token_id); + +// testing::set_caller_address(owner); +// ERC721Impl::approve(ref state, OPERATOR(), token_id); +// utils::drop_event(ZERO()); + +// testing::set_caller_address(OPERATOR()); +// ERC721Impl::safe_transfer_from(ref state, owner, receiver, token_id, DATA(true)); +// assert_event_transfer(owner, receiver, token_id); + +// assert_state_after_transfer(@state,owner, receiver, token_id); +// } + +// #[test] +// #[available_gas(20000000)] +// fn test_safeTransferFrom_approved() { +// let mut state = setup(); +// let receiver = setup_receiver(); +// let token_id = TOKEN_ID; +// let owner = OWNER(); + +// assert_state_before_transfer(@state,owner, receiver, token_id); + +// testing::set_caller_address(owner); +// ERC721Impl::approve(ref state, OPERATOR(), token_id); +// utils::drop_event(ZERO()); + +// testing::set_caller_address(OPERATOR()); +// ERC721CamelOnlyImpl::safeTransferFrom(ref state, owner, receiver, token_id, DATA(true)); +// assert_event_transfer(owner, receiver, token_id); + +// assert_state_after_transfer(@state,owner, receiver, token_id); +// } + +// #[test] +// #[available_gas(20000000)] +// fn test_safe_transfer_from_approved_camel() { +// let mut state = setup(); +// let receiver = setup_camel_receiver(); +// let token_id = TOKEN_ID; +// let owner = OWNER(); + +// assert_state_before_transfer(@state,owner, receiver, token_id); + +// testing::set_caller_address(owner); +// ERC721Impl::approve(ref state, OPERATOR(), token_id); +// utils::drop_event(ZERO()); + +// testing::set_caller_address(OPERATOR()); +// ERC721Impl::safe_transfer_from(ref state, owner, receiver, token_id, DATA(true)); +// assert_event_transfer(owner, receiver, token_id); + +// assert_state_after_transfer(@state,owner, receiver, token_id); +// } + +// #[test] +// #[available_gas(20000000)] +// fn test_safeTransferFrom_approved_camel() { +// let mut state = setup(); +// let receiver = setup_camel_receiver(); +// let token_id = TOKEN_ID; +// let owner = OWNER(); + +// assert_state_before_transfer(@state,owner, receiver, token_id); + +// testing::set_caller_address(owner); +// ERC721Impl::approve(ref state, OPERATOR(), token_id); +// utils::drop_event(ZERO()); + +// testing::set_caller_address(OPERATOR()); +// ERC721CamelOnlyImpl::safeTransferFrom(ref state, owner, receiver, token_id, DATA(true)); +// assert_event_transfer(owner, receiver, token_id); + +// assert_state_after_transfer(@state,owner, receiver, token_id); +// } + +// #[test] +// #[available_gas(20000000)] +// fn test_safe_transfer_from_approved_for_all() { +// let mut state = setup(); +// let receiver = setup_receiver(); +// let token_id = TOKEN_ID; +// let owner = OWNER(); + +// assert_state_before_transfer(@state,owner, receiver, token_id); + +// testing::set_caller_address(owner); +// ERC721Impl::set_approval_for_all(ref state, OPERATOR(), true); +// utils::drop_event(ZERO()); + +// testing::set_caller_address(OPERATOR()); +// ERC721Impl::safe_transfer_from(ref state, owner, receiver, token_id, DATA(true)); +// assert_event_transfer(owner, receiver, token_id); + +// assert_state_after_transfer(@state,owner, receiver, token_id); +// } + +// #[test] +// #[available_gas(20000000)] +// fn test_safeTransferFrom_approved_for_all() { +// let mut state = setup(); +// let receiver = setup_receiver(); +// let token_id = TOKEN_ID; +// let owner = OWNER(); + +// assert_state_before_transfer(@state,owner, receiver, token_id); + +// testing::set_caller_address(owner); +// ERC721Impl::set_approval_for_all(ref state, OPERATOR(), true); +// utils::drop_event(ZERO()); + +// testing::set_caller_address(OPERATOR()); +// ERC721CamelOnlyImpl::safeTransferFrom(ref state, owner, receiver, token_id, DATA(true)); +// assert_event_transfer(owner, receiver, token_id); + +// assert_state_after_transfer(@state,owner, receiver, token_id); +// } + +// #[test] +// #[available_gas(20000000)] +// fn test_safe_transfer_from_approved_for_all_camel() { +// let mut state = setup(); +// let receiver = setup_camel_receiver(); +// let token_id = TOKEN_ID; +// let owner = OWNER(); + +// assert_state_before_transfer(@state,owner, receiver, token_id); + +// testing::set_caller_address(owner); +// ERC721Impl::set_approval_for_all(ref state, OPERATOR(), true); +// utils::drop_event(ZERO()); + +// testing::set_caller_address(OPERATOR()); +// ERC721Impl::safe_transfer_from(ref state, owner, receiver, token_id, DATA(true)); +// assert_event_transfer(owner, receiver, token_id); + +// assert_state_after_transfer(@state,owner, receiver, token_id); +// } + +// #[test] +// #[available_gas(20000000)] +// fn test_safeTransferFrom_approved_for_all_camel() { +// let mut state = setup(); +// let receiver = setup_camel_receiver(); +// let token_id = TOKEN_ID; +// let owner = OWNER(); + +// assert_state_before_transfer(@state,owner, receiver, token_id); + +// testing::set_caller_address(owner); +// ERC721Impl::set_approval_for_all(ref state, OPERATOR(), true); +// utils::drop_event(ZERO()); + +// testing::set_caller_address(OPERATOR()); +// ERC721CamelOnlyImpl::safeTransferFrom(ref state, owner, receiver, token_id, DATA(true)); +// assert_event_transfer(owner, receiver, token_id); + +// assert_state_after_transfer(@state,owner, receiver, token_id); +// } + +// #[test] +// #[available_gas(20000000)] +// #[should_panic(expected: ('ERC721: unauthorized caller',))] +// fn test_safe_transfer_from_unauthorized() { +// let mut state = setup(); +// testing::set_caller_address(OTHER()); +// ERC721Impl::safe_transfer_from(ref state, OWNER(), RECIPIENT(), TOKEN_ID, DATA(true)); +// } + +// #[test] +// #[available_gas(20000000)] +// #[should_panic(expected: ('ERC721: unauthorized caller',))] +// fn test_safeTransferFrom_unauthorized() { +// let mut state = setup(); +// testing::set_caller_address(OTHER()); +// ERC721CamelOnlyImpl::safeTransferFrom(ref state, OWNER(), RECIPIENT(), TOKEN_ID, DATA(true)); +// } + +// +// _transfer +// + +#[test] +#[available_gas(50000000)] +fn test__transfer() { + let mut state = setup(); + let token_id = TOKEN_ID; + let owner = OWNER(); + let recipient = RECIPIENT(); + + assert_state_before_transfer(@state, owner, recipient, token_id); + + InternalImpl::_transfer(ref state, owner, recipient, token_id); + assert_event_transfer(owner, recipient, token_id); + + assert_state_after_transfer(@state, owner, recipient, token_id); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC721: invalid token ID',))] +fn test__transfer_nonexistent() { + let (world, mut state) = STATE(); + InternalImpl::_transfer(ref state, ZERO(), RECIPIENT(), TOKEN_ID); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC721: invalid receiver',))] +fn test__transfer_to_zero() { + let mut state = setup(); + InternalImpl::_transfer(ref state, OWNER(), ZERO(), TOKEN_ID); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC721: wrong sender',))] +fn test__transfer_from_invalid_owner() { + let mut state = setup(); + InternalImpl::_transfer(ref state, RECIPIENT(), OWNER(), TOKEN_ID); +} + +// +// _mint +// + +#[test] +#[available_gas(20000000)] +fn test__mint() { + let (world, mut state) = STATE(); + let recipient = RECIPIENT(); + let token_id = TOKEN_ID; + + assert_state_before_mint(@state, recipient); + InternalImpl::_mint(ref state, recipient, TOKEN_ID); + assert_event_transfer(ZERO(), recipient, token_id); + + assert_state_after_mint(@state, recipient, token_id); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC721: invalid receiver',))] +fn test__mint_to_zero() { + let (world, mut state) = STATE(); + InternalImpl::_mint(ref state, ZERO(), TOKEN_ID); +} + +#[test] +#[available_gas(20000000)] +#[should_panic(expected: ('ERC721: token already minted',))] +fn test__mint_already_exist() { + let mut state = setup(); + InternalImpl::_mint(ref state, RECIPIENT(), TOKEN_ID); +} + +// // +// // _safe_mint +// // + +// #[test] +// #[available_gas(20000000)] +// fn test__safe_mint_to_receiver() { +// let mut state = STATE(); +// let recipient = setup_receiver(); +// let token_id = TOKEN_ID; + +// assert_state_before_mint(@state,recipient); +// InternalImpl::_safe_mint(ref state, recipient, token_id, DATA(true)); +// assert_event_transfer(ZERO(), recipient, token_id); + +// assert_state_after_mint(@state,recipient, token_id); +// } + +// #[test] +// #[available_gas(20000000)] +// fn test__safe_mint_to_receiver_camel() { +// let mut state = STATE(); +// let recipient = setup_camel_receiver(); +// let token_id = TOKEN_ID; + +// assert_state_before_mint(@state,recipient); +// InternalImpl::_safe_mint(ref state, recipient, token_id, DATA(true)); +// assert_event_transfer(ZERO(), recipient, token_id); + +// assert_state_after_mint(@state,recipient, token_id); +// } + +// #[test] +// #[available_gas(20000000)] +// fn test__safe_mint_to_account() { +// let mut state = STATE(); +// let account = setup_account(); +// let token_id = TOKEN_ID; + +// assert_state_before_mint(@state,account); +// InternalImpl::_safe_mint(ref state, account, token_id, DATA(true)); +// assert_event_transfer(ZERO(), account, token_id); + +// assert_state_after_mint(@state,account, token_id); +// } + +// #[test] +// #[available_gas(20000000)] +// fn test__safe_mint_to_account_camel() { +// let mut state = STATE(); +// let account = setup_camel_account(); +// let token_id = TOKEN_ID; + +// assert_state_before_mint(@state,account); +// InternalImpl::_safe_mint(ref state, account, token_id, DATA(true)); +// assert_event_transfer(ZERO(), account, token_id); + +// assert_state_after_mint(@state,account, token_id); +// } + +// #[test] +// #[available_gas(20000000)] +// #[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] +// fn test__safe_mint_to_non_receiver() { +// let mut state = STATE(); +// let recipient = utils::deploy(NonImplementingMock::TEST_CLASS_HASH, array![]); +// let token_id = TOKEN_ID; + +// assert_state_before_mint(@state,recipient); +// InternalImpl::_safe_mint(ref state, recipient, token_id, DATA(true)); +// assert_state_after_mint(@state,recipient, token_id); +// } + +// #[test] +// #[available_gas(20000000)] +// #[should_panic(expected: ('ERC721: safe mint failed',))] +// fn test__safe_mint_to_receiver_failure() { +// let mut state = STATE(); +// let recipient = setup_receiver(); +// let token_id = TOKEN_ID; + +// assert_state_before_mint(@state,recipient); +// InternalImpl::_safe_mint(ref state, recipient, token_id, DATA(false)); +// assert_state_after_mint(@state,recipient, token_id); +// } + +// #[test] +// #[available_gas(20000000)] +// #[should_panic(expected: ('ERC721: safe mint failed',))] +// fn test__safe_mint_to_receiver_failure_camel() { +// let mut state = STATE(); +// let recipient = setup_camel_receiver(); +// let token_id = TOKEN_ID; + +// assert_state_before_mint(@state,recipient); +// InternalImpl::_safe_mint(ref state, recipient, token_id, DATA(false)); +// assert_state_after_mint(@state,recipient, token_id); +// } + +// #[test] +// #[available_gas(20000000)] +// #[should_panic(expected: ('ERC721: invalid receiver',))] +// fn test__safe_mint_to_zero() { +// let mut state = STATE(); +// InternalImpl::_safe_mint(ref state, ZERO(), TOKEN_ID, DATA(true)); +// } + +// #[test] +// #[available_gas(20000000)] +// #[should_panic(expected: ('ERC721: token already minted',))] +// fn test__safe_mint_already_exist() { +// let mut state = setup(); +// InternalImpl::_safe_mint(ref state, RECIPIENT(), TOKEN_ID, DATA(true)); +// } + +// +// _burn +// + +#[test] +#[available_gas(25000000)] +fn test__burn() { + let mut state = setup(); + + InternalImpl::_approve(ref state, OTHER(), TOKEN_ID); + utils::drop_event(ZERO()); + + assert(ERC721Impl::owner_of(@state, TOKEN_ID) == OWNER(), 'Ownership before'); + assert(ERC721Impl::balance_of(@state, OWNER()) == 1, 'Balance of owner before'); + assert(ERC721Impl::get_approved(@state, TOKEN_ID) == OTHER(), 'Approval before'); + + InternalImpl::_burn(ref state, TOKEN_ID); + assert_event_transfer(OWNER(), ZERO(), TOKEN_ID); + + assert( + WorldInteractionsImpl::get_owner_of(@state, TOKEN_ID).address == ZERO(), 'Ownership after' + ); + assert(ERC721Impl::balance_of(@state, OWNER()) == 0, 'Balance of owner after'); + assert( + WorldInteractionsImpl::get_token_approval(@state, TOKEN_ID).address == ZERO(), + 'Approval after' + ); +} + +#[test] +#[available_gas(30000000)] +#[should_panic(expected: ('ERC721: invalid token ID',))] +fn test__burn_nonexistent() { + let (mut world, mut state) = STATE(); + InternalImpl::_burn(ref state, TOKEN_ID); +} + +// +// _set_token_uri +// + +// #[test] +// #[available_gas(20000000)] +// fn test__set_token_uri() { +// let mut state = setup(); + +// assert(ERC721MetadataImpl::token_uri(@state, TOKEN_ID) == 0, 'URI should be 0'); +// InternalImpl::_set_token_uri(ref state, TOKEN_ID, URI); +// assert(ERC721MetadataImpl::token_uri(@state, TOKEN_ID) == URI, 'URI should be set'); +// } + +// #[test] +// #[available_gas(20000000)] +// #[should_panic(expected: ('ERC721: invalid token ID',))] +// fn test__set_token_uri_nonexistent() { +// let mut state = STATE(); +// InternalImpl::_set_token_uri(ref state, TOKEN_ID, URI); +// } + +// +// Helpers +// + +fn assert_state_before_transfer( + state: @ERC721::ContractState, + owner: ContractAddress, + recipient: ContractAddress, + token_id: u256 +) { + assert(ERC721Impl::owner_of(state, token_id) == owner, 'Ownership before'); + assert(ERC721Impl::balance_of(state, owner) == 1, 'Balance of owner before'); + assert(ERC721Impl::balance_of(state, recipient) == 0, 'Balance of recipient before'); +} + +fn assert_state_after_transfer( + state: @ERC721::ContractState, + owner: ContractAddress, + recipient: ContractAddress, + token_id: u256 +) { + assert(ERC721Impl::owner_of(state, token_id) == recipient, 'Ownership after'); + assert(ERC721Impl::balance_of(state, owner) == 0, 'Balance of owner after'); + assert(ERC721Impl::balance_of(state, recipient) == 1, 'Balance of recipient after'); + assert(ERC721Impl::get_approved(state, token_id) == ZERO(), 'Approval not implicitly reset'); +} + +fn assert_state_before_mint(state: @ERC721::ContractState, recipient: ContractAddress) { + assert(ERC721Impl::balance_of(state, recipient) == 0, 'Balance of recipient before'); +} + +fn assert_state_after_mint( + state: @ERC721::ContractState, recipient: ContractAddress, token_id: u256 +) { + assert(ERC721Impl::owner_of(state, token_id) == recipient, 'Ownership after'); + assert(ERC721Impl::balance_of(state, recipient) == 1, 'Balance of recipient after'); + assert(ERC721Impl::get_approved(state, token_id) == ZERO(), 'Approval implicitly set'); +} + +fn assert_event_approval_for_all( + owner: ContractAddress, operator: ContractAddress, approved: bool +) { + let event = utils::pop_log::(ZERO()).unwrap(); + assert(event.owner == owner, 'Invalid `owner`'); + assert(event.operator == operator, 'Invalid `operator`'); + assert(event.approved == approved, 'Invalid `approved`'); + utils::assert_no_events_left(ZERO()); +} + +fn assert_event_approval(owner: ContractAddress, approved: ContractAddress, token_id: u256) { + let event = utils::pop_log::(ZERO()).unwrap(); + assert(event.owner == owner, 'Invalid `owner`'); + assert(event.approved == approved, 'Invalid `approved`'); + assert(event.token_id == token_id, 'Invalid `token_id`'); + utils::assert_no_events_left(ZERO()); +} + +fn assert_event_transfer(from: ContractAddress, to: ContractAddress, token_id: u256) { + let event = utils::pop_log::(ZERO()).unwrap(); + assert(event.from == from, 'Invalid `from`'); + assert(event.to == to, 'Invalid `to`'); + assert(event.token_id == token_id, 'Invalid `token_id`'); + utils::assert_no_events_left(ZERO()); +} diff --git a/presets/src/lib.cairo b/presets/src/lib.cairo new file mode 100644 index 00000000..95234237 --- /dev/null +++ b/presets/src/lib.cairo @@ -0,0 +1,13 @@ +mod erc721 { + mod models; + mod erc721; + use erc721::ERC721; + #[cfg(test)] + mod tests; +} + +#[cfg(test)] +mod tests { + mod constants; + mod utils; +} \ No newline at end of file diff --git a/presets/src/tests/constants.cairo b/presets/src/tests/constants.cairo new file mode 100644 index 00000000..6487fc18 --- /dev/null +++ b/presets/src/tests/constants.cairo @@ -0,0 +1,60 @@ +use starknet::ContractAddress; +use starknet::contract_address_const; + +const NAME: felt252 = 'NAME'; +const SYMBOL: felt252 = 'SYMBOL'; +const DECIMALS: u8 = 18_u8; +const SUPPLY: u256 = 2000; +const VALUE: u256 = 300; +const ROLE: felt252 = 'ROLE'; +const OTHER_ROLE: felt252 = 'OTHER_ROLE'; +const URI: felt252 = 'URI'; +const TOKEN_ID: u256 = 21; +const TOKEN_AMOUNT: u256 = 42; +const TOKEN_ID_2: u256 = 2; +const TOKEN_AMOUNT_2: u256 = 69; +const PUBKEY: felt252 = 'PUBKEY'; + +fn ADMIN() -> ContractAddress { + contract_address_const::<'ADMIN'>() +} + +fn AUTHORIZED() -> ContractAddress { + contract_address_const::<'AUTHORIZED'>() +} + +fn ZERO() -> ContractAddress { + contract_address_const::<0>() +} + +fn CALLER() -> ContractAddress { + contract_address_const::<'CALLER'>() +} + +fn OWNER() -> ContractAddress { + contract_address_const::<'OWNER'>() +} + +fn NEW_OWNER() -> ContractAddress { + contract_address_const::<'NEW_OWNER'>() +} + +fn OTHER() -> ContractAddress { + contract_address_const::<'OTHER'>() +} + +fn OTHER_ADMIN() -> ContractAddress { + contract_address_const::<'OTHER_ADMIN'>() +} + +fn SPENDER() -> ContractAddress { + contract_address_const::<'SPENDER'>() +} + +fn RECIPIENT() -> ContractAddress { + contract_address_const::<'RECIPIENT'>() +} + +fn OPERATOR() -> ContractAddress { + contract_address_const::<'OPERATOR'>() +} diff --git a/presets/src/tests/test_erc1155.cairo b/presets/src/tests/test_erc1155.cairo new file mode 100644 index 00000000..5ce73b82 --- /dev/null +++ b/presets/src/tests/test_erc1155.cairo @@ -0,0 +1,629 @@ +use zeroable::Zeroable; +use traits::{Into, Default, IndexView}; +use option::OptionTrait; +use array::ArrayTrait; +use serde::Serde; +use starknet::ContractAddress; +use starknet::testing::set_contract_address; + +use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait}; + +use dojo_erc::tests::test_utils::impersonate; +use dojo_erc::tests::test_erc1155_utils::{ + spawn_world, deploy_erc1155, deploy_default, deploy_testcase1, ZERO, USER1, USER2, DEPLOYER, + PROXY +}; + +use dojo_erc::erc165::interface::IERC165_ID; +use dojo_erc::erc1155::interface::{ + IERC1155A, IERC1155ADispatcher, IERC1155ADispatcherTrait, IERC1155_ID, IERC1155_METADATA_ID, + IERC1155_RECEIVER_ID +}; + +use dojo_erc::erc1155::erc1155::ERC1155::{Event, TransferSingle, TransferBatch, ApprovalForAll}; + + +#[test] +#[available_gas(30000000)] +fn test_deploy() { + let world = spawn_world(DEPLOYER()); + let erc1155_address = deploy_erc1155(world, DEPLOYER(), 'uri', 'seed-42'); + let erc1155 = IERC1155ADispatcher { contract_address: erc1155_address }; + assert(erc1155.owner() == DEPLOYER(), 'invalid owner'); +} + +#[test] +#[available_gas(30000000)] +fn test_deploy_default() { + let (world, erc1155) = deploy_default(); + assert(erc1155.owner() == DEPLOYER(), 'invalid owner'); +} + + +// +// supports_interface +// + +#[test] +#[available_gas(30000000)] +fn test_should_support_interfaces() { + let (world, erc1155) = deploy_default(); + + assert(erc1155.supports_interface(IERC165_ID) == true, 'should support erc165'); + assert(erc1155.supports_interface(IERC1155_ID) == true, 'should support erc1155'); + assert( + erc1155.supports_interface(IERC1155_METADATA_ID) == true, 'should support erc1155_metadata' + ); +} + +// +// uri +// + +#[test] +#[available_gas(30000000)] +fn test_uri() { + let (world, erc1155) = deploy_default(); + assert(erc1155.uri(64) == 'uri', 'invalid uri'); +} + + +// +// behaves like an ERC1155 +// + +// +// balance_of +// +#[test] +#[available_gas(30000000)] +#[should_panic(expected: ('ERC1155: invalid owner address', 'ENTRYPOINT_FAILED',))] +fn test_balance_of_zero_address() { + //reverts when queried about the zero address + + let (world, erc1155) = deploy_default(); + erc1155.balance_of(ZERO(), 0); // should panic +} + +#[test] +#[available_gas(30000000)] +fn test_balance_of_empty_balance() { + // when accounts don't own tokens + // returns zero for given addresses + let (world, erc1155) = deploy_default(); + assert(erc1155.balance_of(USER1(), 0) == 0, 'should be 0'); + assert(erc1155.balance_of(USER1(), 69) == 0, 'should be 0'); + assert(erc1155.balance_of(USER2(), 0) == 0, 'should be 0'); +} + +#[test] +#[available_gas(30000000)] +fn test_balance_with_tokens() { + // when accounts own some tokens + // returns the amount of tokens owned by the given addresses + let (world, erc1155) = deploy_default(); + + erc1155.mint(USER1(), 0, 1, array![]); + erc1155.mint(USER1(), 69, 42, array![]); + erc1155.mint(USER2(), 69, 5, array![]); + + assert(erc1155.balance_of(USER1(), 0) == 1, 'should be 1'); + assert(erc1155.balance_of(USER1(), 69) == 42, 'should be 42'); + assert(erc1155.balance_of(USER2(), 69) == 5, 'should be 5'); +} + +// +// balance_of_batch +// + +#[test] +#[available_gas(30000000)] +#[should_panic(expected: ('ERC1155: invalid length', 'ENTRYPOINT_FAILED',))] +fn test_balance_of_batch_with_invalid_input() { + // reverts when input arrays don't match up + let (world, erc1155) = deploy_default(); + erc1155.balance_of_batch(array![USER1(), USER2()], array![0]); + erc1155.balance_of_batch(array![USER1()], array![0, 1, 2]); +} + +#[test] +#[available_gas(30000000)] +#[should_panic(expected: ('ERC1155: invalid owner address', 'ENTRYPOINT_FAILED',))] +fn test_balance_of_batch_address_zero() { + // reverts when input arrays don't match up + let (world, erc1155) = deploy_default(); + erc1155.balance_of_batch(array![USER1(), ZERO()], array![0, 1]); +} + +#[test] +#[available_gas(30000000)] +fn test_balance_of_batch_empty_account() { + // when accounts don't own tokens + // returns zeros for each account + let (world, erc1155) = deploy_default(); + let balances = erc1155.balance_of_batch(array![USER1(), USER1(), USER1()], array![0, 1, 5]); + let bals = @balances; + assert(balances.len() == 3, 'should be 3'); + assert(bals[0] == @0_u256, 'should be 0'); + assert(bals[1] == @0_u256, 'should be 0'); + assert(bals[2] == @0_u256, 'should be 0'); +} + +#[test] +#[available_gas(30000000)] +fn test_balance_of_batch_with_tokens() { + // when accounts own some tokens + // returns amounts owned by each account in order passed + let (world, erc1155) = deploy_default(); + + erc1155.mint(USER1(), 0, 1, array![]); + erc1155.mint(USER1(), 69, 42, array![]); + erc1155.mint(USER2(), 69, 2, array![]); + + let balances = erc1155.balance_of_batch(array![USER1(), USER1(), USER2()], array![0, 69, 69]); + let bals = @balances; + assert(balances.len() == 3, 'should be 3'); + assert(bals[0] == @1_u256, 'should be 1'); + assert(bals[1] == @42_u256, 'should be 42'); + assert(bals[2] == @2_u256, 'should be 2'); +} + +#[test] +#[available_gas(30000000)] +fn test_balance_of_batch_with_tokens_2() { + // when accounts own some tokens + // returns multiple times the balance of the same address when asked + let (world, erc1155) = deploy_default(); + + erc1155.mint(USER1(), 0, 1, array![]); + erc1155.mint(USER2(), 69, 2, array![]); + + let balances = erc1155.balance_of_batch(array![USER1(), USER2(), USER1()], array![0, 69, 0]); + let bals = @balances; + assert(balances.len() == 3, 'should be 3'); + assert(bals[0] == @1_u256, 'should be 1'); + assert(bals[1] == @2_u256, 'should be 2'); + assert(bals[2] == @1_u256, 'should be 1'); +} + + +// +// balance_of_batch +// + +#[test] +#[available_gas(30000000)] +fn test_set_approval_for_all() { + // sets approval status which can be queried via is_approved_for_all + let (world, erc1155) = deploy_default(); + impersonate(USER1()); + + erc1155.set_approval_for_all(PROXY(), true); + assert(erc1155.is_approved_for_all(USER1(), PROXY()) == true, 'should be true'); +} + +#[test] +#[available_gas(30000000)] +fn test_set_approval_for_all_emit_event() { + // set_approval_for_all emits ApprovalForAll event + let (world, erc1155) = deploy_default(); + impersonate(USER1()); + + erc1155.set_approval_for_all(PROXY(), true); + + // ApprovalForAll + assert( + @starknet::testing::pop_log(erc1155.contract_address) + .unwrap() == @Event::ApprovalForAll( + ApprovalForAll { owner: USER1(), operator: PROXY(), approved: true } + ), + 'invalid ApprovalForAll event' + ); +} + + +#[test] +#[available_gas(30000000)] +fn test_set_unset_approval_for_all() { + // sets approval status which can be queried via is_approved_for_all + let (world, erc1155) = deploy_default(); + impersonate(USER1()); + + erc1155.set_approval_for_all(PROXY(), true); + assert(erc1155.is_approved_for_all(USER1(), PROXY()) == true, 'should be true'); + erc1155.set_approval_for_all(PROXY(), false); + assert(erc1155.is_approved_for_all(USER1(), PROXY()) == false, 'should be false'); +} + +#[test] +#[available_gas(30000000)] +#[should_panic()] +fn test_set_approval_for_all_on_self() { + // reverts if attempting to approve self as an operator + let (world, erc1155) = deploy_default(); + impersonate(USER1()); + + erc1155.set_approval_for_all(USER1(), true); // should panic +} + +// +// safe_transfer_from +// + +#[test] +#[available_gas(30000000)] +#[should_panic()] +fn test_safe_transfer_from_more_than_balance() { + // reverts when transferring more than balance + let (world, erc1155) = deploy_testcase1(); + + impersonate(USER1()); + + erc1155.safe_transfer_from(USER1(), USER2(), 1, 999, array![]); // should panic +} + +#[test] +#[available_gas(30000000)] +#[should_panic()] +fn test_safe_transfer_to_zero() { + // reverts when transferring to zero address + let (world, erc1155) = deploy_testcase1(); + + impersonate(USER1()); + + erc1155.safe_transfer_from(USER1(), ZERO(), 1, 1, array![]); // should panic +} + +#[test] +#[available_gas(50000000)] +fn test_safe_transfer_debit_sender() { + // debits transferred balance from sender + let (world, erc1155) = deploy_testcase1(); + + impersonate(USER1()); + + let balance_before = erc1155.balance_of(USER1(), 1); + erc1155.safe_transfer_from(USER1(), USER2(), 1, 1, array![]); + let balance_after = erc1155.balance_of(USER1(), 1); + + assert(balance_after == balance_before - 1, 'invalid balance after'); +} + +#[test] +#[available_gas(50000000)] +fn test_safe_transfer_credit_receiver() { + // credits transferred balance to receiver + let (world, erc1155) = deploy_testcase1(); + + impersonate(USER1()); + + let balance_before = erc1155.balance_of(USER2(), 1); + erc1155.safe_transfer_from(USER1(), USER2(), 1, 1, array![]); + let balance_after = erc1155.balance_of(USER2(), 1); + + assert(balance_after == balance_before + 1, 'invalid balance after'); +} + +#[test] +#[available_gas(50000000)] +fn test_safe_transfer_preserve_existing_balances() { + // preserves existing balances which are not transferred by multiTokenHolder + let (world, erc1155) = deploy_testcase1(); + + // impersonate user1 + impersonate(USER1()); + + let balance_before_2 = erc1155.balance_of(USER2(), 2); + let balance_before_3 = erc1155.balance_of(USER2(), 3); + erc1155.safe_transfer_from(USER1(), USER2(), 1, 1, array![]); + let balance_after_2 = erc1155.balance_of(USER2(), 2); + let balance_after_3 = erc1155.balance_of(USER2(), 3); + + assert(balance_after_2 == balance_before_2, 'should be equal'); + assert(balance_after_3 == balance_before_3, 'should be equal'); +} + +#[test] +#[available_gas(30000000)] +#[should_panic()] +fn test_safe_transfer_from_unapproved_operator() { + // when called by an operator on behalf of the multiTokenHolder + // when operator is not approved by multiTokenHolder + + let (world, erc1155) = deploy_testcase1(); + + impersonate(USER2()); + + erc1155.safe_transfer_from(USER1(), USER2(), 1, 1, array![]); // should panic +} + +#[test] +#[available_gas(50000000)] +fn test_safe_transfer_from_approved_operator() { + // when called by an operator on behalf of the multiTokenHolder + // when operator is approved by multiTokenHolder + let (world, erc1155) = deploy_testcase1(); + + impersonate(PROXY()); + + let balance_before = erc1155.balance_of(USER1(), 1); + erc1155.safe_transfer_from(USER1(), USER2(), 1, 2, array![]); + let balance_after = erc1155.balance_of(USER1(), 1); + + assert(balance_after == balance_before - 2, 'invalid balance'); +} + +#[test] +#[available_gas(50000000)] +fn test_safe_transfer_from_approved_operator_preserve_operator_balance() { + // when called by an operator on behalf of the multiTokenHolder + // preserves operator's balances not involved in the transfer + let (world, erc1155) = deploy_testcase1(); + + impersonate(PROXY()); + + let balance_before_1 = erc1155.balance_of(PROXY(), 1); + let balance_before_2 = erc1155.balance_of(PROXY(), 2); + let balance_before_3 = erc1155.balance_of(PROXY(), 3); + erc1155.safe_transfer_from(USER1(), USER2(), 1, 2, array![]); + let balance_after_1 = erc1155.balance_of(PROXY(), 1); + let balance_after_2 = erc1155.balance_of(PROXY(), 2); + let balance_after_3 = erc1155.balance_of(PROXY(), 3); + + assert(balance_before_1 == balance_after_1, 'should be equal'); + assert(balance_before_2 == balance_after_2, 'should be equal'); + assert(balance_before_3 == balance_after_3, 'should be equal'); +} + + +#[test] +#[available_gas(50000000)] +#[should_panic] +fn test_safe_transfer_from_zero_address() { + let (world, erc1155) = deploy_testcase1(); + + impersonate(USER1()); + + erc1155.safe_transfer_from(ZERO(), USER1(), 1, 1, array![]); +} + +// +// safe_batch_transfer_from +// + +#[test] +#[available_gas(50000000)] +#[should_panic] +fn test_safe_batch_transfer_from_more_than_balance() { + // reverts when transferring amount more than any of balances + let (world, erc1155) = deploy_testcase1(); + + impersonate(USER1()); + + erc1155 + .safe_batch_transfer_from(USER1(), USER2(), array![1, 2, 3], array![1, 999, 1], array![]); +} + + +#[test] +#[available_gas(50000000)] +#[should_panic] +fn test_safe_batch_transfer_from_mismatching_array_len() { + // reverts when ids array length doesn't match amounts array length + let (world, erc1155) = deploy_testcase1(); + + impersonate(USER1()); + + erc1155.safe_batch_transfer_from(USER1(), USER2(), array![1, 2, 3], array![1, 1], array![]); +} + + +#[test] +#[available_gas(50000000)] +#[should_panic] +fn test_safe_batch_transfer_from_to_zero_address() { + // reverts when transferring to zero address + let (world, erc1155) = deploy_testcase1(); + + impersonate(USER1()); + + erc1155.safe_batch_transfer_from(USER1(), ZERO(), array![1, 2], array![1, 1], array![]); +} + + +#[test] +#[available_gas(60000000)] +fn test_safe_batch_transfer_from_debits_sender() { + // debits transferred balances from sender + let (world, erc1155) = deploy_testcase1(); + + impersonate(USER1()); + + let balance_before_1 = erc1155.balance_of(USER1(), 1); + let balance_before_2 = erc1155.balance_of(USER1(), 2); + let balance_before_3 = erc1155.balance_of(USER1(), 3); + erc1155 + .safe_batch_transfer_from(USER1(), USER2(), array![1, 2, 3], array![1, 10, 20], array![]); + let balance_after_1 = erc1155.balance_of(USER1(), 1); + let balance_after_2 = erc1155.balance_of(USER1(), 2); + let balance_after_3 = erc1155.balance_of(USER1(), 3); + + assert(balance_before_1 - 1 == balance_after_1, 'invalid balance'); + assert(balance_before_2 - 10 == balance_after_2, 'invalid balance'); + assert(balance_before_3 - 20 == balance_after_3, 'invalid balance'); +} + + +#[test] +#[available_gas(60000000)] +fn test_safe_batch_transfer_from_credits_recipient() { + // credits transferred balances to receiver + let (world, erc1155) = deploy_testcase1(); + + impersonate(USER1()); + + let balance_before_1 = erc1155.balance_of(USER2(), 1); + let balance_before_2 = erc1155.balance_of(USER2(), 2); + let balance_before_3 = erc1155.balance_of(USER2(), 3); + erc1155 + .safe_batch_transfer_from(USER1(), USER2(), array![1, 2, 3], array![1, 10, 20], array![]); + let balance_after_1 = erc1155.balance_of(USER2(), 1); + let balance_after_2 = erc1155.balance_of(USER2(), 2); + let balance_after_3 = erc1155.balance_of(USER2(), 3); + + assert(balance_before_1 + 1 == balance_after_1, 'invalid balance'); + assert(balance_before_2 + 10 == balance_after_2, 'invalid balance'); + assert(balance_before_1 + 20 == balance_after_3, 'invalid balance'); +} + + +#[test] +#[available_gas(50000000)] +#[should_panic] +fn test_safe_batch_transfer_from_unapproved_operator() { + // when called by an operator on behalf of the multiTokenHolder + // when operator is not approved by multiTokenHolder + + let (world, erc1155) = deploy_testcase1(); + + impersonate(USER2()); + + erc1155.safe_batch_transfer_from(USER1(), USER2(), array![1, 2], array![1, 10], array![]); +} + +#[test] +#[available_gas(60000000)] +fn test_safe_batch_transfer_from_approved_operator_preserve_operator_balance() { + // when called by an operator on behalf of the multiTokenHolder + // preserves operator's balances not involved in the transfer + + let (world, erc1155) = deploy_testcase1(); + + impersonate(PROXY()); + + let balance_before_1 = erc1155.balance_of(PROXY(), 1); + let balance_before_2 = erc1155.balance_of(PROXY(), 2); + let balance_before_3 = erc1155.balance_of(PROXY(), 3); + + erc1155 + .safe_batch_transfer_from(USER1(), USER2(), array![1, 2, 3], array![1, 10, 20], array![]); + + let balance_after_1 = erc1155.balance_of(PROXY(), 1); + let balance_after_2 = erc1155.balance_of(PROXY(), 2); + let balance_after_3 = erc1155.balance_of(PROXY(), 3); + + assert(balance_before_1 == balance_after_1, 'should be equal'); + assert(balance_before_2 == balance_after_2, 'should be equal'); + assert(balance_before_3 == balance_after_3, 'should be equal'); +} + +#[test] +#[available_gas(50000000)] +#[should_panic] +fn test_safe_batch_transfer_from_zero_address() { + let (world, erc1155) = deploy_testcase1(); + + impersonate(USER1()); + + erc1155.safe_batch_transfer_from(ZERO(), USER1(), array![1, 2], array![1, 1], array![]); +} + + +#[test] +#[available_gas(50000000)] +fn test_safe_batch_transfer_emit_transfer_batch_event() { + let (world, erc1155) = deploy_default(); + + // user1 token_id 1 x 10 + erc1155.mint(USER1(), 1, 10, array![]); + // user1 token_id 2 x 20 + erc1155.mint(USER1(), 2, 20, array![]); + + impersonate(USER1()); + + erc1155.safe_batch_transfer_from(USER1(), USER2(), array![1, 2], array![1, 10], array![]); + + let _: Event = starknet::testing::pop_log(erc1155.contract_address) + .unwrap(); // unpop erc1155.mint(USER1(), 1, 10, array![]); + let _: Event = starknet::testing::pop_log(erc1155.contract_address) + .unwrap(); // unpop erc1155.mint(USER1(), 2, 20, array![]); + + // TransferBatch + assert( + @starknet::testing::pop_log(erc1155.contract_address) + .unwrap() == @Event::TransferBatch( + TransferBatch { + operator: USER1(), + from: USER1(), + to: USER2(), + ids: array![1, 2], + values: array![1, 10] + } + ), + 'invalid TransferBatch event' + ); +} + + +// +// burn +// + +#[test] +#[available_gas(90000000)] +#[should_panic] +fn test_burn_non_existing_token_id() { + //reverts when burning a non-existent token id + let (world, erc1155) = deploy_default(); + + impersonate(USER1()); + erc1155.burn(USER1(), 69, 1); // should panic +} + + +#[test] +#[available_gas(90000000)] +fn test_burn_emit_transfer_single_event() { + // burn should emit event + let (world, erc1155) = deploy_default(); + + erc1155.mint(USER1(), 69, 5, array![]); + assert(erc1155.balance_of(USER1(), 69) == 5, 'invalid balance'); + + impersonate(USER1()); + + erc1155.burn(USER1(), 69, 1); + assert(erc1155.balance_of(USER1(), 69) == 4, 'invalid balance'); + + let _: Event = starknet::testing::pop_log(erc1155.contract_address) + .unwrap(); // unpop erc1155.mint(USER1(), 69,5,array![]) + + // TransferSingle + assert( + @starknet::testing::pop_log(erc1155.contract_address) + .unwrap() == @Event::TransferSingle( + TransferSingle { operator: USER1(), from: USER1(), to: ZERO(), id: 69, value: 1 } + ), + 'invalid TransferSingle event' + ); +} + + +#[test] +#[available_gas(90000000)] +#[should_panic] +fn test_burn_more_than_owned() { + // reverts when burning more tokens than owned + let (world, erc1155) = deploy_default(); + erc1155.mint(USER1(), 69, 10, array![]); + + impersonate(USER1()); + + erc1155.burn(USER1(), 69, 1); + erc1155.burn(USER1(), 69, 10); // should panic +} +// TODO : to be continued + +// TODO : add test if we support IERC1155Receiver + + diff --git a/presets/src/tests/test_erc721.cairo b/presets/src/tests/test_erc721.cairo new file mode 100644 index 00000000..fc23abc4 --- /dev/null +++ b/presets/src/tests/test_erc721.cairo @@ -0,0 +1,862 @@ +use core::zeroable::Zeroable; +use core::traits::{Into, Default}; +use array::ArrayTrait; +use serde::Serde; +use starknet::ContractAddress; +use starknet::testing::set_contract_address; +use option::OptionTrait; + +use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait}; + +use dojo_erc::tests::test_utils::impersonate; +use dojo_erc::tests::test_erc721_utils::{ + spawn_world, deploy_erc721, deploy_default, deploy_testcase1, USER1, USER2, USER3, DEPLOYER, + ZERO, PROXY +}; + + +use dojo_erc::erc165::interface::IERC165_ID; +use dojo_erc::erc721::interface::{ + IERC721, IERC721ADispatcher, IERC721ADispatcherTrait, IERC721_ID, IERC721_METADATA_ID +}; +use dojo_erc::erc721::erc721::ERC721::{Event, Transfer, Approval, ApprovalForAll}; +// actually it's possible to mint -> burn -> mint -> ... +// todo : add Minted component to keep track of minted ids + +#[test] +#[available_gas(30000000)] +fn test_deploy() { + let world = spawn_world(DEPLOYER()); + let erc721_address = deploy_erc721(world, DEPLOYER(), 'name', 'symbol', 'uri', 'seed-42'); + let erc721 = IERC721ADispatcher { contract_address: erc721_address }; + + assert(erc721.owner() == DEPLOYER(), 'invalid owner'); + assert(erc721.name() == 'name', 'invalid name'); + assert(erc721.symbol() == 'symbol', 'invalid symbol'); +} + + +#[test] +#[available_gas(30000000)] +fn test_deploy_default() { + let (world, erc721) = deploy_default(); + assert(erc721.name() == 'name', 'invalid name'); +} + +// +// supports_interface +// + +#[test] +#[available_gas(30000000)] +fn test_should_support_interfaces() { + let (world, erc721) = deploy_default(); + + assert(erc721.supports_interface(IERC165_ID) == true, 'should support erc165'); + assert(erc721.supports_interface(IERC721_ID) == true, 'should support erc721'); + assert( + erc721.supports_interface(IERC721_METADATA_ID) == true, 'should support erc721_metadata' + ); +} + + +// +// behaves like an ERC721 +// + +// +// balance_of +// + +use debug::PrintTrait; + +#[test] +#[available_gas(60000000)] +fn test_balance_of_with_tokens() { + // returns the amount of tokens owned by the given address + + let (world, erc721) = deploy_testcase1(); + assert(erc721.balance_of(USER1()) == 3, 'should be 3'); + assert(erc721.balance_of(PROXY()) == 4, 'should be 4'); +} + +#[test] +#[available_gas(60000000)] +fn test_balance_of_with_no_tokens() { + // when the given address does not own any tokens + + let (world, erc721) = deploy_testcase1(); + assert(erc721.balance_of(USER3()) == 0, 'should be 0'); +} + + +#[test] +#[available_gas(50000000)] +#[should_panic] +fn test_balance_of_zero_address() { + // when querying the zero address + + let (world, erc721) = deploy_testcase1(); + erc721.balance_of(ZERO()); +} + +// +// owner_of +// + +#[test] +#[available_gas(90000000)] +fn test_owner_of_existing_id() { + // when the given token ID was tracked by this token = for existing id + + let (world, erc721) = deploy_testcase1(); + assert(erc721.owner_of(1) == USER1(), 'should be user1'); + assert(erc721.owner_of(2) == USER1(), 'should be user1'); + assert(erc721.owner_of(3) == USER1(), 'should be user1'); + + assert(erc721.owner_of(10) == PROXY(), 'should be proxy'); + assert(erc721.owner_of(11) == PROXY(), 'should be proxy'); + assert(erc721.owner_of(12) == PROXY(), 'should be proxy'); + assert(erc721.owner_of(13) == PROXY(), 'should be proxy'); +} + + +#[test] +#[available_gas(90000000)] +#[should_panic] +fn test_owner_of_non_existing_id() { + // when the given token ID was not tracked by this token = non existing id + + let (world, erc721) = deploy_testcase1(); + let owner_of_0 = erc721.owner_of(0); // should panic +} + +// +// transfers +// + +#[test] +#[available_gas(90000000)] +fn test_transfer_ownership() { + // transfers the ownership of the given token ID to the given address + + let (world, erc721) = deploy_testcase1(); + + impersonate(USER1()); + + let owner_of_1 = erc721.owner_of(1); + // transfer token_id 1 to user2 + erc721.transfer(USER2(), 1); + assert(erc721.owner_of(1) == USER2(), 'invalid owner'); +} + +#[test] +#[available_gas(90000000)] +fn test_transfer_event() { + // emits a Transfer event + + let (world, erc721) = deploy_default(); + + // mint + erc721.mint(USER1(), 42); + + impersonate(USER1()); + + // transfer token_id 1 to user2 + erc721.transfer(USER2(), 42); + + impersonate(USER2()); + erc721.burn(42); + + // mint + assert( + @starknet::testing::pop_log(erc721.contract_address) + .unwrap() == @Event::Transfer(Transfer { from: ZERO(), to: USER1(), token_id: 42 }), + 'invalid Transfer event' + ); + // transfer + assert( + @starknet::testing::pop_log(erc721.contract_address) + .unwrap() == @Event::Transfer(Transfer { from: USER1(), to: USER2(), token_id: 42 }), + 'invalid Transfer event' + ); + // burn + assert( + @starknet::testing::pop_log(erc721.contract_address) + .unwrap() == @Event::Transfer(Transfer { from: USER2(), to: ZERO(), token_id: 42 }), + 'invalid Transfer event' + ); +} + + +#[test] +#[available_gas(90000000)] +fn test_transfer_clear_approval() { + // clears the approval for the token ID + + let (world, erc721) = deploy_testcase1(); + + impersonate(USER1()); + + erc721.approve(PROXY(), 1); + assert(erc721.get_approved(1) == PROXY(), 'should be proxy'); + + // transfer token_id 1 to user2 + erc721.transfer(USER2(), 1); + assert(erc721.get_approved(1).is_zero(), 'should be zero'); +} + +#[test] +#[available_gas(90000000)] +fn test_transfer_adjusts_owners_balances() { + // adjusts owners balances + + let (world, erc721) = deploy_testcase1(); + + impersonate(USER1()); + + let balance_user1_before = erc721.balance_of(USER1()); + let balance_user2_before = erc721.balance_of(USER2()); + + // transfer token_id 1 to user2 + erc721.transfer(USER2(), 1); + + let balance_user1_after = erc721.balance_of(USER1()); + let balance_user2_after = erc721.balance_of(USER2()); + + assert(balance_user1_after == balance_user1_before - 1, 'invalid user1 balance'); + assert(balance_user2_after == balance_user2_before + 1, 'invalid user2 balance'); +} + + +#[test] +#[available_gas(90000000)] +fn test_transfer_from_approved() { + // when called by the approved individual + + let (world, erc721) = deploy_testcase1(); + + impersonate(USER1()); + + //user1 approve user2 for token_id 2 + erc721.approve(USER2(), 2); + + impersonate(USER2()); + + erc721.transfer_from(USER1(), USER2(), 2); + assert(erc721.owner_of(2) == USER2(), 'invalid owner'); +} + +#[test] +#[available_gas(90000000)] +fn test_transfer_from_approved_operator() { + // when called by the operator + + let (world, erc721) = deploy_testcase1(); + + impersonate(USER1()); + + //user1 set_approval_for_all for proxy + erc721.set_approval_for_all(PROXY(), true); + + impersonate(PROXY()); + + erc721.transfer_from(USER1(), USER2(), 2); + assert(erc721.owner_of(2) == USER2(), 'invalid owner'); +} + +#[test] +#[available_gas(90000000)] +fn test_transfer_from_owner_without_approved() { + // when called by the owner without an approved user + + let (world, erc721) = deploy_testcase1(); + + impersonate(USER1()); + + erc721.approve(ZERO(), 2); + + erc721.transfer_from(USER1(), USER2(), 2); + assert(erc721.owner_of(2) == USER2(), 'invalid owner'); +} + + +#[test] +#[available_gas(90000000)] +fn test_transfer_to_owner() { + // when sent to the owner + + let (world, erc721) = deploy_testcase1(); + + impersonate(USER1()); + + let balance_before = erc721.balance_of(USER1()); + + assert(erc721.owner_of(3) == USER1(), 'invalid owner'); + erc721.transfer(USER1(), 3); + + // keeps ownership of the token + assert(erc721.owner_of(3) == USER1(), 'invalid owner'); + + // clears the approval for the token ID + assert(erc721.get_approved(3) == ZERO(), 'invalid approved'); + + //emits only a transfer event : cumbersome to test with pop_log + + //keeps the owner balance + let balance_after = erc721.balance_of(USER1()); + assert(balance_before == balance_after, 'invalid balance') +} + + +#[test] +#[available_gas(90000000)] +#[should_panic] +fn test_transfer_when_previous_owner_is_incorrect() { + // when the address of the previous owner is incorrect + + let (world, erc721) = deploy_testcase1(); + + impersonate(USER1()); + + //user2 owner token_id 10 + erc721.transfer_from(USER1(), PROXY(), 10); // should panic +} + +#[test] +#[available_gas(90000000)] +#[should_panic] +fn test_transfer_when_sender_not_authorized() { + // when the sender is not authorized for the token id + let (world, erc721) = deploy_testcase1(); + + impersonate(PROXY()); + + //proxy is not authorized for USER2 + erc721.transfer_from(USER2(), PROXY(), 20); // should panic +} + +#[test] +#[available_gas(90000000)] +#[should_panic] +fn test_transfer_when_token_id_doesnt_exists() { + // when the sender is not authorized for the token id + let (world, erc721) = deploy_testcase1(); + + impersonate(PROXY()); + + //proxy is authorized for USER1 but token_id 50 doesnt exists + erc721.transfer_from(USER1(), PROXY(), 50); // should panic +} + + +#[test] +#[available_gas(90000000)] +#[should_panic] +fn test_transfer_to_address_zero() { + // when the address to transfer the token to is the zero address + let (world, erc721) = deploy_testcase1(); + + impersonate(USER1()); + + erc721.transfer(ZERO(), 1); // should panic +} + +// +// approval +// + +// when clearing approval + +#[test] +#[available_gas(90000000)] +fn test_approval_when_clearing_with_prior_approval() { + // -when there was a prior approval + let (world, erc721) = deploy_default(); + + erc721.mint(USER1(), 42); + + impersonate(USER1()); + + erc721.approve(PROXY(), 42); + + //revoke approve + erc721.approve(ZERO(), 42); + + // clears approval for the token + assert(erc721.get_approved(42) == ZERO(), 'invalid approved'); + + // emits an approval event + let _: Event = starknet::testing::pop_log(erc721.contract_address).unwrap(); // unpop mint + let _: Event = starknet::testing::pop_log(erc721.contract_address) + .unwrap(); // unpop approve PROXY + + // approve ZERO + assert( + @starknet::testing::pop_log(erc721.contract_address) + .unwrap() == @Event::Approval(Approval { owner: USER1(), to: ZERO(), token_id: 42 }), + 'invalid Approval event' + ); +} + +#[test] +#[available_gas(90000000)] +fn test_approval_when_clearing_without_prior_approval() { + // when clearing approval + // -when there was no prior approval + let (world, erc721) = deploy_default(); + + erc721.mint(USER1(), 42); + + impersonate(USER1()); + + //revoke approve + erc721.approve(ZERO(), 42); + + // updates approval for the token + assert(erc721.get_approved(42) == ZERO(), 'invalid approved'); + + let _: Event = starknet::testing::pop_log(erc721.contract_address).unwrap(); // unpop mint + + // approve ZERO + assert( + @starknet::testing::pop_log(erc721.contract_address) + .unwrap() == @Event::Approval(Approval { owner: USER1(), to: ZERO(), token_id: 42 }), + 'invalid Approval event' + ); +} + + +// when approving a non-zero address + +#[test] +#[available_gas(90000000)] +fn test_approval_non_zero_address_with_prior_approval() { + // -when there was a prior approval + let (world, erc721) = deploy_default(); + + erc721.mint(USER1(), 42); + + impersonate(USER1()); + erc721.approve(PROXY(), 42); + + // user1 approves user3 + erc721.approve(USER3(), 42); + + // set approval for the token + assert(erc721.get_approved(42) == USER3(), 'invalid approved'); + + // emits an approval event + let _: Event = starknet::testing::pop_log(erc721.contract_address).unwrap(); // unpop mint + let _: Event = starknet::testing::pop_log(erc721.contract_address) + .unwrap(); // unpop approve PROXY + + // approve USER3 + assert( + @starknet::testing::pop_log(erc721.contract_address) + .unwrap() == @Event::Approval(Approval { owner: USER1(), to: USER3(), token_id: 42 }), + 'invalid Approval event' + ); +} + +#[test] +#[available_gas(90000000)] +fn test_approval_non_zero_address_with_no_prior_approval() { + // -when there was no prior approval + let (world, erc721) = deploy_default(); + + erc721.mint(USER1(), 42); + + impersonate(USER1()); + + // user1 approves user3 + erc721.approve(USER3(), 42); + + // set approval for the token + assert(erc721.get_approved(42) == USER3(), 'invalid approved'); + + // emits an approval event + let _: Event = starknet::testing::pop_log(erc721.contract_address).unwrap(); // unpop mint + + // approve USER3 + assert( + @starknet::testing::pop_log(erc721.contract_address) + .unwrap() == @Event::Approval(Approval { owner: USER1(), to: USER3(), token_id: 42 }), + 'invalid Approval event' + ); +} + + +#[test] +#[available_gas(90000000)] +#[should_panic] +fn test_approval_self_approve() { + // when the address that receives the approval is the owner + let (world, erc721) = deploy_default(); + + erc721.mint(USER1(), 42); + + impersonate(USER1()); + + // user1 approves user1 + erc721.approve(USER1(), 42); // should panic +} + +#[test] +#[available_gas(90000000)] +#[should_panic] +fn test_approval_not_owned() { + // when the sender does not own the given token ID + + let (world, erc721) = deploy_testcase1(); + + impersonate(USER1()); + + // user1 approves user2 for token 20 + erc721.approve(USER2(), 20); // should panic +} + + +#[test] +#[available_gas(90000000)] +#[should_panic] +fn test_approval_from_approved_sender() { + // when the sender is approved for the given token ID + + let (world, erc721) = deploy_testcase1(); + + impersonate(USER1()); + + // user1 approve user3 + erc721.approve(USER3(), 1); + + impersonate(USER3()); + + // (ERC721: approve caller is not token owner or approved for all) + erc721.approve(USER2(), 1); // should panic +} + + +#[test] +#[available_gas(90000000)] +fn test_approval_from_approved_operator() { + // when the sender is an operator + let (world, erc721) = deploy_default(); + + erc721.mint(USER1(), 50); + + impersonate(USER1()); + + erc721.set_approval_for_all(PROXY(), true); + + impersonate(PROXY()); + + // proxy approves user2 for token 20 + erc721.approve(USER2(), 50); + + assert(erc721.get_approved(50) == USER2(), 'invalid approval'); + + let _: Event = starknet::testing::pop_log(erc721.contract_address).unwrap(); // unpop mint + let _: Event = starknet::testing::pop_log(erc721.contract_address) + .unwrap(); // unpop set_approval_for_all + + // approve + assert( + @starknet::testing::pop_log(erc721.contract_address) + .unwrap() == @Event::Approval(Approval { owner: USER1(), to: USER2(), token_id: 50 }), + 'invalid Approval event' + ); +} + + +#[test] +#[available_gas(90000000)] +#[should_panic] +fn test_approval_unexisting_id() { + // when the given token ID does not exist + let (world, erc721) = deploy_testcase1(); + + impersonate(USER1()); + + // user1 approve user3 + erc721.approve(USER3(), 69); // should panic +} + +// +// approval_for_all +// + +#[test] +#[available_gas(90000000)] +fn test_approval_for_all_operator_is_not_owner_no_operator_approval() { + // when the operator willing to approve is not the owner + // -when there is no operator approval set by the sender + let (world, erc721) = deploy_default(); + + impersonate(USER2()); + + // user2 set_approval_for_all PROXY + erc721.set_approval_for_all(PROXY(), true); + + assert(erc721.is_approved_for_all(USER2(), PROXY()) == true, 'invalid is_approved_for_all'); + + // ApproveForAll + assert( + @starknet::testing::pop_log(erc721.contract_address) + .unwrap() == @Event::ApprovalForAll( + ApprovalForAll { owner: USER2(), operator: PROXY(), approved: true } + ), + 'invalid ApprovalForAll event' + ); +} + +#[test] +#[available_gas(90000000)] +fn test_approval_for_all_operator_is_not_owner_from_not_approved() { + // when the operator willing to approve is not the owner + // -when the operator was set as not approved + let (world, erc721) = deploy_default(); + + impersonate(USER2()); + + erc721.set_approval_for_all(PROXY(), false); + + // user2 set_approval_for_all PROXY + erc721.set_approval_for_all(PROXY(), true); + + assert(erc721.is_approved_for_all(USER2(), PROXY()) == true, 'invalid is_approved_for_all'); + + let _: Event = starknet::testing::pop_log(erc721.contract_address) + .unwrap(); // unpop set_approval_for_all(PROXY(), false) + + // ApproveForAll + assert( + @starknet::testing::pop_log(erc721.contract_address) + .unwrap() == @Event::ApprovalForAll( + ApprovalForAll { owner: USER2(), operator: PROXY(), approved: true } + ), + 'invalid ApprovalForAll event' + ); +} + +#[test] +#[available_gas(90000000)] +fn test_approval_for_all_operator_is_not_owner_can_unset_approval_for_all() { + // when the operator willing to approve is not the owner + // can unset the operator approval + let (world, erc721) = deploy_default(); + + impersonate(USER2()); + + erc721.set_approval_for_all(PROXY(), false); + erc721.set_approval_for_all(PROXY(), true); + assert(erc721.is_approved_for_all(USER2(), PROXY()) == true, 'invalid is_approved_for_all'); + erc721.set_approval_for_all(PROXY(), false); + assert(erc721.is_approved_for_all(USER2(), PROXY()) == false, 'invalid is_approved_for_all'); + + let _: Event = starknet::testing::pop_log(erc721.contract_address) + .unwrap(); // unpop set_approval_for_all(PROXY(), false) + let _: Event = starknet::testing::pop_log(erc721.contract_address) + .unwrap(); // unpop set_approval_for_all(PROXY(), true) + + // ApproveForAll + assert( + @starknet::testing::pop_log(erc721.contract_address) + .unwrap() == @Event::ApprovalForAll( + ApprovalForAll { owner: USER2(), operator: PROXY(), approved: false } + ), + 'invalid ApprovalForAll event' + ); +} + +#[test] +#[available_gas(90000000)] +fn test_approval_for_all_operator_with_operator_already_approved() { + // when the operator willing to approve is not the owner + // when the operator was already approved + let (world, erc721) = deploy_default(); + + impersonate(USER2()); + + erc721.set_approval_for_all(PROXY(), true); + assert(erc721.is_approved_for_all(USER2(), PROXY()) == true, 'invalid is_approved_for_all'); + erc721.set_approval_for_all(PROXY(), true); + assert(erc721.is_approved_for_all(USER2(), PROXY()) == true, 'invalid is_approved_for_all'); + + let _: Event = starknet::testing::pop_log(erc721.contract_address) + .unwrap(); // unpop set_approval_for_all(PROXY(), true) + + // ApproveForAll + assert( + @starknet::testing::pop_log(erc721.contract_address) + .unwrap() == @Event::ApprovalForAll( + ApprovalForAll { owner: USER2(), operator: PROXY(), approved: true } + ), + 'invalid ApprovalForAll event' + ); +} + + +#[test] +#[available_gas(90000000)] +#[should_panic] +fn test_approval_for_all_with_owner_as_operator() { + // when the operator is the owner + + let (world, erc721) = deploy_default(); + + impersonate(USER1()); + + erc721.set_approval_for_all(USER1(), true); // should panic +} + + +// +// get_approved +// + +#[test] +#[available_gas(90000000)] +#[should_panic] +fn test_get_approved_unexisting_token() { + let (world, erc721) = deploy_default(); + + erc721.get_approved(420); // should panic +} + + +#[test] +#[available_gas(90000000)] +fn test_get_approved_with_existing_token() { + let (world, erc721) = deploy_default(); + + erc721.mint(USER1(), 420); + assert(erc721.get_approved(420) == ZERO(), 'invalid get_approved'); +} + + +#[test] +#[available_gas(90000000)] +fn test_get_approved_with_existing_token_and_approval() { + let (world, erc721) = deploy_default(); + + erc721.mint(USER1(), 420); + + impersonate(USER1()); + + erc721.approve(PROXY(), 420); + assert(erc721.get_approved(420) == PROXY(), 'invalid get_approved'); +} + +// +// mint +// + +#[test] +#[available_gas(90000000)] +#[should_panic] +fn test_mint_to_address_zero() { + // reverts with a null destination address + + let (world, erc721) = deploy_default(); + + erc721.mint(ZERO(), 69); // should panic +} + + +#[test] +#[available_gas(90000000)] +fn test_mint() { + // reverts with a null destination address + + let (world, erc721) = deploy_default(); + + erc721.mint(USER1(), 69); + + assert(erc721.balance_of(USER1()) == 1, 'invalid balance'); + + // Transfer + assert( + @starknet::testing::pop_log(erc721.contract_address) + .unwrap() == @Event::Transfer(Transfer { from: ZERO(), to: USER1(), token_id: 69 }), + 'invalid Transfer event' + ); +} + +#[test] +#[available_gas(90000000)] +#[should_panic] +fn test_mint_existing_token_id() { + // reverts with a null destination address + + let (world, erc721) = deploy_default(); + + erc721.mint(USER1(), 69); + erc721.mint(USER1(), 69); //should panic +} + + +// +// burn +// + +#[test] +#[available_gas(90000000)] +#[should_panic] +fn test_burn_non_existing_token_id() { + //reverts when burning a non-existent token id + let (world, erc721) = deploy_default(); + erc721.burn(69); // should panic +} + + +#[test] +#[available_gas(90000000)] +fn test_burn_emit_events() { + // burn should emit event + let (world, erc721) = deploy_default(); + + erc721.mint(USER1(), 69); + assert(erc721.balance_of(USER1()) == 1, 'invalid balance'); + + impersonate(USER1()); + + erc721.burn(69); + assert(erc721.balance_of(USER1()) == 0, 'invalid balance'); + + let _: Event = starknet::testing::pop_log(erc721.contract_address) + .unwrap(); // unpop erc721.mint(USER1(), 69) + + // Transfer + assert( + @starknet::testing::pop_log(erc721.contract_address) + .unwrap() == @Event::Transfer(Transfer { from: USER1(), to: ZERO(), token_id: 69 }), + 'invalid Transfer event' + ); +} + + +#[test] +#[available_gas(90000000)] +#[should_panic] +fn test_burn_same_id_twice() { + // reverts when burning a token id that has been deleted + let (world, erc721) = deploy_default(); + erc721.mint(USER1(), 69); + erc721.burn(69); + erc721.burn(69); // should panic +} + +// +// token_uri +// + +#[test] +#[available_gas(90000000)] +#[should_panic] +fn test_token_uri_for_non_existing_token_id() { + // reverts when queried for non existent token id + let (world, erc721) = deploy_default(); + erc721.token_uri(1234); // should panic +} + diff --git a/presets/src/tests/utils.cairo b/presets/src/tests/utils.cairo new file mode 100644 index 00000000..99e95669 --- /dev/null +++ b/presets/src/tests/utils.cairo @@ -0,0 +1,32 @@ +use core::result::ResultTrait; +use starknet::class_hash::Felt252TryIntoClassHash; +use starknet::ContractAddress; +use starknet::testing; + +fn deploy(contract_class_hash: felt252, calldata: Array) -> ContractAddress { + let (address, _) = starknet::deploy_syscall( + contract_class_hash.try_into().unwrap(), 0, calldata.span(), false + ) + .unwrap(); + address +} + +/// Pop the earliest unpopped logged event for the contract as the requested type +/// and checks there's no more data left on the event, preventing unaccounted params. +/// Indexed event members are currently not supported, so they are ignored. +fn pop_log, impl TEvent: starknet::Event>( + address: ContractAddress +) -> Option { + let (mut keys, mut data) = testing::pop_log_raw(address)?; + let ret = starknet::Event::deserialize(ref keys, ref data); + assert(data.is_empty(), 'Event has extra data'); + ret +} + +fn assert_no_events_left(address: ContractAddress) { + assert(testing::pop_log_raw(address).is_none(), 'Events remaining on queue'); +} + +fn drop_event(address: ContractAddress) { + testing::pop_log_raw(address); +}