diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml new file mode 100644 index 00000000..23702ad0 --- /dev/null +++ b/.github/actions/setup/action.yml @@ -0,0 +1,15 @@ +name: 'Setup' +description: 'Setup tooling' +runs: + using: "composite" + steps: + - name: Install dojoup + run: curl -L https://install.dojoengine.org | bash + shell: bash + + - name: Install dojo + run: | + /home/runner/.config/.dojo/bin/dojoup --version ${{ env.DOJO_VERSION }} + sudo mv /home/runner/.config/.dojo/bin/katana /usr/local/bin/ + sudo mv /home/runner/.config/.dojo/bin/sozo /usr/local/bin/ + shell: bash diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..b2f52c9b --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,67 @@ +name: Test + +on: [push, pull_request] + +env: + DOJO_VERSION: v0.3.3 + +jobs: + check: + runs-on: ubuntu-latest + name: Check format + steps: + - uses: actions/checkout@v4 + - uses: software-mansion/setup-scarb@v1 + - name: Format + run: scarb fmt --package random --check + shell: bash + + build: + runs-on: ubuntu-latest + name: Build package + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/setup + - name: Build + run: sozo build + shell: bash + + algebra: + runs-on: ubuntu-latest + name: Test algebra crate + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/setup + - name: Test + run: sozo test -f algebra + shell: bash + + defi: + runs-on: ubuntu-latest + name: Test defi crate + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/setup + - name: Test + run: sozo test -f defi + shell: bash + + random: + runs-on: ubuntu-latest + name: Test random crate + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/setup + - name: Test + run: sozo test -f random + shell: bash + + security: + runs-on: ubuntu-latest + name: Test security crate + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/setup + - name: Test + run: sozo test -f security + shell: bash \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..16db3829 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.vscode +target \ No newline at end of file diff --git a/README.md b/README.md index d6adead3..7bc1da1a 100644 --- a/README.md +++ b/README.md @@ -1 +1 @@ -# origami \ No newline at end of file +# Origami \ No newline at end of file diff --git a/Scarb.lock b/Scarb.lock new file mode 100644 index 00000000..508ef783 --- /dev/null +++ b/Scarb.lock @@ -0,0 +1,47 @@ +# 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 = "defi" +version = "0.0.0" +dependencies = [ + "cubit", +] + +[[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 = "random" +version = "0.0.0" +dependencies = [ + "dojo", +] + +[[package]] +name = "security" +version = "0.0.0" +dependencies = [ + "dojo", +] diff --git a/Scarb.toml b/Scarb.toml new file mode 100644 index 00000000..c32f1f57 --- /dev/null +++ b/Scarb.toml @@ -0,0 +1,14 @@ +[workspace] +name = "origami" +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", +] + +[workspace.dependencies] +dojo = { git = "https://github.com/dojoengine/dojo.git", tag = "v0.3.3" } \ No newline at end of file diff --git a/crates/algebra/.gitignore b/crates/algebra/.gitignore new file mode 100644 index 00000000..1de56593 --- /dev/null +++ b/crates/algebra/.gitignore @@ -0,0 +1 @@ +target \ No newline at end of file diff --git a/crates/algebra/Scarb.lock b/crates/algebra/Scarb.lock new file mode 100644 index 00000000..0d969403 --- /dev/null +++ b/crates/algebra/Scarb.lock @@ -0,0 +1,14 @@ +# 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 = "physics" +version = "0.0.0" +dependencies = [ + "cubit", +] diff --git a/crates/algebra/Scarb.toml b/crates/algebra/Scarb.toml new file mode 100644 index 00000000..084c5744 --- /dev/null +++ b/crates/algebra/Scarb.toml @@ -0,0 +1,8 @@ +[package] +name = "algebra" +version = "0.0.0" +description = "Implementations of a algebra library." +homepage = "https://github.com/dojoengine/origami/tree/crates/algebra" + +[dependencies] +cubit = { git = "https://github.com/influenceth/cubit", rev = "b459053" } diff --git a/crates/algebra/src/lib.cairo b/crates/algebra/src/lib.cairo new file mode 100644 index 00000000..46a1516d --- /dev/null +++ b/crates/algebra/src/lib.cairo @@ -0,0 +1,3 @@ +mod vec2; +mod vector; +mod matrix; \ No newline at end of file diff --git a/crates/algebra/src/matrix.cairo b/crates/algebra/src/matrix.cairo new file mode 100644 index 00000000..6612f9d1 --- /dev/null +++ b/crates/algebra/src/matrix.cairo @@ -0,0 +1,415 @@ +use zeroable::Zeroable; + +#[derive(Copy, Drop)] +struct Matrix { + data: Span, + rows: u8, + cols: u8, +} + +mod errors { + const INVALID_INDEX: felt252 = 'Matrix: index out of bounds'; + const INVALID_DIMENSION: felt252 = 'Matrix: invalid dimension'; + const INVALID_MATRIX_INVERSION: felt252 = 'Matrix: matrix not invertible'; +} + +trait MatrixTrait { + fn new(data: Span, rows: u8, cols: u8) -> Matrix; + + fn get(ref self: Matrix, row: u8, col: u8) -> T; + + fn transpose(ref self: Matrix) -> Matrix; + + fn minor(ref self: Matrix, exclude_row: u8, exclude_col: u8) -> Matrix; + + fn det(ref self: Matrix) -> T; + + fn inv(ref self: Matrix) -> Matrix; +} + +impl MatrixImpl< + T, + +Mul, + +Div, + +Add, + +AddEq, + +Sub, + +SubEq, + +Neg, + +Zeroable, + +Copy, + +Drop, +> of MatrixTrait { + fn new(data: Span, rows: u8, cols: u8) -> Matrix { + // [Check] Data is consistent with dimensions + assert(data.len() == (rows * cols).into(), errors::INVALID_DIMENSION); + Matrix { data, rows, cols } + } + + fn get(ref self: Matrix, row: u8, col: u8) -> T { + let index: u8 = row * self.cols + col; + *self.data.get(index.into()).expect(errors::INVALID_INDEX).unbox() + } + + fn transpose(ref self: Matrix) -> Matrix { + let mut values = array![]; + let max_index: u8 = self.rows * self.cols; + let mut index: u8 = 0; + loop { + if index == max_index { + break; + } + let row = index % self.rows; + let col = index / self.rows; + values.append(self.get(row, col)); + index += 1; + }; + MatrixTrait::new(values.span(), self.cols, self.rows) + } + + fn minor(ref self: Matrix, exclude_row: u8, exclude_col: u8) -> Matrix { + let mut values = array![]; + let mut index: u8 = 0; + let max_index: u8 = self.rows * self.cols; + loop { + if index == max_index { + break; + }; + + let row = index / self.cols; + let col = index % self.cols; + + if row != exclude_row && col != exclude_col { + values.append(self.get(row, col)); + }; + + index += 1; + }; + + MatrixTrait::new(values.span(), self.cols - 1, self.rows - 1) + } + + fn det(ref self: Matrix) -> T { + // [Check] Matrix is square + assert(self.rows == self.cols, errors::INVALID_DIMENSION); + if self.rows == 1 { + return self.get(0, 0); + } + + if self.rows == 2 { + return (self.get(0, 0) * self.get(1, 1)) - (self.get(0, 1) * self.get(1, 0)); + } + + let mut det: T = Zeroable::zero(); + let mut col: u8 = 0; + loop { + if col >= self.cols { + break; + } + + let coef = self.get(0, col); + let mut minor = self.minor(0, col); + if col % 2 == 0 { + det += coef * minor.det(); + } else { + det -= coef * minor.det(); + }; + + col += 1; + }; + + return det; + } + + fn inv(ref self: Matrix) -> Matrix { + let determinant = self.det(); + assert(determinant.is_non_zero(), errors::INVALID_MATRIX_INVERSION); + + let mut values: Array = array![]; + + let max_index: u8 = self.rows * self.cols; + let mut index: u8 = 0; + + loop { + if index == max_index { + break; + } + + // Extract row and column from the linear index + let col = index / self.rows; + let row = index % self.rows; + + // Compute the cofactor + let mut minor = self.minor(row, col); + let cofactor = if (row + col) % 2 == 0 { + minor.det() + } else { + -minor.det() + }; + values.append(cofactor / determinant); + + index += 1; + }; + + MatrixTrait::new(values.span(), self.cols, self.rows) + } +} + +impl MatrixAdd< + T, + +Mul, + +Div, + +Add, + +AddEq, + +Sub, + +SubEq, + +Neg, + +Zeroable, + +Copy, + +Drop, +> of Add> { + fn add(mut lhs: Matrix, mut rhs: Matrix) -> Matrix { + // [Check] Dimesions are compatible + assert(lhs.rows == rhs.rows && lhs.cols == rhs.cols, errors::INVALID_DIMENSION); + let mut values = array![]; + let max_index = lhs.rows * lhs.cols; + let mut index = 0; + loop { + if index == max_index { + break; + } + let row = index / lhs.cols; + let col = index % lhs.cols; + values.append(lhs.get(row, col) + rhs.get(row, col)); + index += 1; + }; + MatrixTrait::new(values.span(), lhs.rows, lhs.cols) + } +} + +impl MatrixSub< + T, + +Mul, + +Div, + +Add, + +AddEq, + +Sub, + +SubEq, + +Neg, + +Zeroable, + +Copy, + +Drop, +> of Sub> { + fn sub(mut lhs: Matrix, mut rhs: Matrix) -> Matrix { + // [Check] Dimesions are compatible + assert(lhs.rows == rhs.rows && lhs.cols == rhs.cols, errors::INVALID_DIMENSION); + let mut values = array![]; + let max_index = lhs.rows * lhs.cols; + let mut index = 0; + loop { + if index == max_index { + break; + } + let row = index / lhs.cols; + let col = index % lhs.cols; + values.append(lhs.get(row, col) - rhs.get(row, col)); + index += 1; + }; + MatrixTrait::new(values.span(), lhs.rows, lhs.cols) + } +} + +impl MatrixMul< + T, + +Mul, + +Div, + +Add, + +AddEq, + +Sub, + +SubEq, + +Neg, + +Zeroable, + +Copy, + +Drop, +> of Mul> { + fn mul(mut lhs: Matrix, mut rhs: Matrix) -> Matrix { + // [Check] Dimesions are compatible + assert(lhs.cols == rhs.rows, errors::INVALID_DIMENSION); + let mut values = array![]; + let max_index = lhs.rows * rhs.cols; + let mut index: u8 = 0; + loop { + if index == max_index { + break; + } + + let row = index / rhs.cols; + let col = index % rhs.cols; + + let mut sum: T = Zeroable::zero(); + let mut k: u8 = 0; + loop { + if k == lhs.cols { + break; + } + + sum += lhs.get(row, k) * rhs.get(k, col); + k += 1; + }; + values.append(sum); + index += 1; + }; + + MatrixTrait::new(values.span(), lhs.rows, rhs.cols) + } +} + +#[cfg(test)] +mod tests { + use core::traits::TryInto; + use super::{Matrix, MatrixTrait}; + use debug::PrintTrait; + + impl I128Zeroable of Zeroable { + fn zero() -> i128 { + 0 + } + fn is_zero(self: i128) -> bool { + self == 0 + } + fn is_non_zero(self: i128) -> bool { + self != 0 + } + } + + impl I128Div of Div { + fn div(lhs: i128, rhs: i128) -> i128 { + let lhs_u256: u256 = Into::::into(lhs.into()); + let rhs_u256: u256 = Into::::into(rhs.into()); + let div: felt252 = (lhs_u256 / rhs_u256).try_into().unwrap(); + div.try_into().unwrap() + } + } + + #[test] + #[available_gas(1_000_000)] + fn test_matrix_get() { + let rows: u8 = 3; + let cols: u8 = 4; + let values: Array = array![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]; + let mut matrix: Matrix = MatrixTrait::new(values.span(), rows, cols); + assert(matrix.get(0, 1) == 2, 'Matrix: get failed'); + assert(matrix.get(2, 3) == 12, 'Matrix: get failed'); + } + + #[test] + #[available_gas(1_000_000)] + fn test_matrix_transpose() { + let rows: u8 = 2; + let cols: u8 = 3; + let values: Array = array![1, 2, 3, 4, 5, 6]; + let mut matrix: Matrix = MatrixTrait::new(values.span(), rows, cols); + let mut transposed = matrix.transpose(); + assert(transposed.get(0, 1) == 4, 'Matrix: transpose failed'); + assert(transposed.get(2, 1) == 6, 'Matrix: transpose failed'); + } + + #[test] + #[available_gas(1_000_000)] + fn test_matrix_addition() { + let rows: u8 = 2; + let cols: u8 = 3; + let values: Array = array![1, 2, 3, 4, 5, 6]; + let mut matrix1 = MatrixTrait::new(values.span(), rows, cols); + let mut matrix2 = MatrixTrait::new(values.span(), rows, cols); + let mut result = matrix1 + matrix2; + assert(result.get(0, 0) == 2, 'Matrix: addition failed'); + assert(result.get(1, 1) == 10, 'Matrix: addition failed'); + } + + #[test] + #[available_gas(1_000_000)] + fn test_matrix_subtraction() { + let rows: u8 = 2; + let cols: u8 = 3; + let values: Array = array![1, 2, 3, 4, 5, 6]; + let mut matrix1 = MatrixTrait::new(values.span(), rows, cols); + let values: Array = array![7, 8, 9, 10, 11, 12]; + let mut matrix2 = MatrixTrait::new(values.span(), rows, cols); + let mut result = matrix1 - matrix2; + assert(result.get(0, 0) == -6, 'Matrix: subtraction failed'); + assert(result.get(1, 1) == -6, 'Matrix: subtraction failed'); + } + + #[test] + #[available_gas(10_000_000)] + fn test_matrix_square_multiplication() { + let size: u8 = 2; + let values: Array = array![1, 2, 3, 4]; + let mut matrix1 = MatrixTrait::new(values.span(), size, size); + let mut matrix2 = MatrixTrait::new(values.span(), size, size); + let mut result = matrix1 * matrix2; + assert(result.get(0, 0) == 7, 'Matrix: multiplication failed'); + assert(result.get(0, 1) == 10, 'Matrix: multiplication failed'); + assert(result.get(1, 0) == 15, 'Matrix: multiplication failed'); + assert(result.get(1, 1) == 22, 'Matrix: multiplication failed'); + } + + #[test] + #[available_gas(10_000_000)] + fn test_matrix_rectangle_multiplication() { + let values: Array = array![1, 2, 3, 4, 5, 6]; + let mut matrix1 = MatrixTrait::new(values.span(), 2, 3); + let mut matrix2 = MatrixTrait::new(values.span(), 3, 2); + let mut result = matrix1 * matrix2; + assert(result.get(0, 0) == 22, 'Matrix: multiplication failed'); + assert(result.get(0, 1) == 28, 'Matrix: multiplication failed'); + assert(result.get(1, 0) == 49, 'Matrix: multiplication failed'); + assert(result.get(1, 1) == 64, 'Matrix: multiplication failed'); + } + + #[test] + #[available_gas(5_000_000)] + fn test_matrix_determinant_2x2() { + let values: Array = array![4, 3, 1, 2]; + let mut matrix = MatrixTrait::new(values.span(), 2, 2); + assert(matrix.det() == 5, 'Matrix: det computation failed'); + } + + #[test] + #[available_gas(10_000_000)] + fn test_matrix_determinant_3x3() { + let values: Array = array![6, 1, 1, 4, -2, 5, 2, 8, 7]; + let mut matrix = MatrixTrait::new(values.span(), 3, 3); + assert(matrix.det() == -306, 'Matrix: det computation failed'); + } + + #[test] + #[available_gas(10_000_000)] + fn test_matrix_inverse_2x2() { + let values: Array = array![1, 2, 0, 1]; + let mut matrix = MatrixTrait::new(values.span(), 2, 2); + let mut inverse = matrix.inv(); + assert(inverse.get(0, 0) == 1, 'Matrix: inversion failed'); + assert(inverse.get(0, 1) == -2, 'Matrix: inversion failed'); + assert(inverse.get(1, 0) == 0, 'Matrix: inversion failed'); + assert(inverse.get(1, 1) == 1, 'Matrix: inversion failed'); + } + + #[test] + #[available_gas(10_000_000)] + fn test_matrix_inverse_3x3() { + let values: Array = array![1, 1, 0, 0, 1, 0, 0, 1, 1]; + let mut matrix = MatrixTrait::new(values.span(), 3, 3); + let mut inverse = matrix.inv(); + assert(inverse.get(0, 0) == 1, 'Matrix: inversion failed'); + assert(inverse.get(0, 1) == -1, 'Matrix: inversion failed'); + assert(inverse.get(0, 2) == 0, 'Matrix: inversion failed'); + assert(inverse.get(1, 0) == 0, 'Matrix: inversion failed'); + assert(inverse.get(1, 1) == 1, 'Matrix: inversion failed'); + assert(inverse.get(1, 2) == 0, 'Matrix: inversion failed'); + assert(inverse.get(2, 0) == 0, 'Matrix: inversion failed'); + 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 new file mode 100644 index 00000000..116ef2f1 --- /dev/null +++ b/crates/algebra/src/vec2.cairo @@ -0,0 +1,269 @@ +use cubit::f128::types::fixed::{Fixed, FixedTrait, ONE_u128}; + +struct Vec2 { + x: T, + y: T +} + +impl Vec2Copy> of Copy>; +impl Vec2Drop> of Drop>; + +trait Vec2Trait { + // Constructors + fn new(x: T, y: T) -> Vec2; + fn splat(self: T) -> Vec2; + // Masks + fn select(mask: Vec2, if_true: Vec2, if_false: Vec2) -> Vec2; + // Math + fn dot, impl TAdd: Add>(self: Vec2, rhs: Vec2) -> T; + fn dot_into_vec, impl TAdd: Add>(self: Vec2, rhs: Vec2) -> Vec2; + // Swizzles + fn xy(self: Vec2) -> Vec2; + fn xx(self: Vec2) -> Vec2; + fn yx(self: Vec2) -> Vec2; + fn yy(self: Vec2) -> Vec2; +} + +impl Vec2Impl, impl TDrop: Drop> of Vec2Trait { + // Constructors + + /// Creates a new vector. + #[inline(always)] + fn new(x: T, y: T) -> Vec2 { + Vec2 { x: x, y: y } + } + /// Creates a vector with all elements set to `v`. + #[inline(always)] + fn splat(self: T) -> Vec2 { + Vec2 { x: self, y: self } + } + + // Masks + + /// Creates a vector from the elements in `if_true` and `if_false`, + /// selecting which to use for each element of `self`. + /// + /// A true element in the mask uses the corresponding element from + /// `if_true`, and false uses the element from `if_false`. + #[inline(always)] + fn select(mask: Vec2, if_true: Vec2, if_false: Vec2) -> Vec2 { + Vec2 { + x: if mask.x { + if_true.x + } else { + if_false.x + }, + y: if mask.y { + if_true.y + } else { + if_false.y + }, + } + } + + // Math + + /// Computes the dot product of `self` and `rhs` . + // #[inline(always)] is not allowed for functions with impl generic parameters. + fn dot, impl TAdd: Add>(self: Vec2, rhs: Vec2) -> T { + (self.x * rhs.x) + (self.y * rhs.y) + } + /// Returns a vector where every component is the dot product + /// of `self` and `rhs`. + fn dot_into_vec, impl TAdd: Add>(self: Vec2, rhs: Vec2) -> Vec2 { + Vec2Trait::splat(Vec2Trait::dot(self, rhs)) + } + + // Swizzles + /// Vec2 -> Vec2 + #[inline(always)] + fn xx(self: Vec2) -> Vec2 { + Vec2 { x: self.x, y: self.x, } + } + + #[inline(always)] + fn xy(self: Vec2) -> Vec2 { + Vec2 { x: self.x, y: self.y, } + } + + #[inline(always)] + fn yx(self: Vec2) -> Vec2 { + Vec2 { x: self.y, y: self.x, } + } + + #[inline(always)] + fn yy(self: Vec2) -> Vec2 { + Vec2 { x: self.y, y: self.y, } + } +} + + +#[cfg(test)] +mod tests { + // Local imports + + use super::{FixedTrait, ONE_u128, Vec2Trait}; + + #[test] + #[available_gas(2000000)] + fn test_new() { + let var1_pos = FixedTrait::new(ONE_u128, false); + let var2_neg = FixedTrait::new(2 * ONE_u128, true); + + // with Fixed type + let vec2 = Vec2Trait::new(var1_pos, var2_neg); + assert(vec2.x.mag == ONE_u128, 'invalid x.mag'); + assert(vec2.x.sign == false, 'invalid x.sign'); + assert(vec2.y.mag == 2 * ONE_u128, 'invalid y.mag'); + assert(vec2.y.sign == true, 'invalid y.sign'); + + // with bool + let bvec2tf = Vec2Trait::new(true, false); + assert(bvec2tf.x == true, 'invalid new x'); + assert(bvec2tf.y == false, 'invalid new y'); + } + + #[test] + #[available_gas(2000000)] + fn test_splat() { + let var = FixedTrait::new(ONE_u128, false); + + // with Fixed type + let vec2 = Vec2Trait::splat(var); + assert(vec2.x.mag == ONE_u128, 'invalid x.mag'); + assert(vec2.x.sign == false, 'invalid x.sign'); + assert(vec2.y.mag == ONE_u128, 'invalid y.mag'); + assert(vec2.y.sign == false, 'invalid y.sign'); + + // with bool + let bvec2tt = Vec2Trait::splat(true); + assert(bvec2tt.x == true, 'invalid x'); + assert(bvec2tt.y == true, 'invalid y'); + } + + #[test] + #[available_gas(2000000)] + fn test_select() { + let var1_pos = FixedTrait::new(ONE_u128, false); + let var2_neg = FixedTrait::new(2 * ONE_u128, true); + let var3_neg = FixedTrait::new(3 * ONE_u128, true); + let var4_pos = FixedTrait::new(4 * ONE_u128, false); + + let vec2a = Vec2Trait::new(var1_pos, var2_neg); + let vec2b = Vec2Trait::new(var3_neg, var4_pos); + + let mask = Vec2Trait::new(true, false); + let vec2 = Vec2Trait::select(mask, vec2a, vec2b); + assert(vec2.x.mag == ONE_u128, 'invalid x.mag'); + assert(vec2.x.sign == false, 'invalid x.sign'); + assert(vec2.y.mag == 4 * ONE_u128, 'invalid y.mag'); + assert(vec2.y.sign == false, 'invalid y.sign'); + + let mask = Vec2Trait::new(false, true); + let vec2 = Vec2Trait::select(mask, vec2a, vec2b); + assert(vec2.x.mag == 3 * ONE_u128, 'invalid x.mag'); + assert(vec2.x.sign == true, 'invalid x.sign'); + assert(vec2.y.mag == 2 * ONE_u128, 'invalid y.mag'); + assert(vec2.y.sign == true, 'invalid y.sign'); + } + + #[test] + #[available_gas(2000000)] + fn test_dot() { + let var1_pos = FixedTrait::new(ONE_u128, false); + let var2_neg = FixedTrait::new(2 * ONE_u128, true); + let var3_neg = FixedTrait::new(3 * ONE_u128, true); + let var4_pos = FixedTrait::new(4 * ONE_u128, false); + + let vec2a = Vec2Trait::new(var1_pos, var2_neg); + let vec2b = Vec2Trait::new(var3_neg, var4_pos); + + let a_dot_b = vec2a.dot(vec2b); + assert(a_dot_b.mag == 11 * ONE_u128, 'invalid mag'); + assert(a_dot_b.sign == true, 'invalid sign'); + + let a_dot_b = Vec2Trait::dot(vec2a, vec2b); // alt syntax + assert(a_dot_b.mag == 11 * ONE_u128, 'invalid mag'); + assert(a_dot_b.sign == true, 'invalid sign'); + } + + #[test] + #[available_gas(2000000)] + fn test_dot_into_vec() { + let var1_pos = FixedTrait::new(ONE_u128, false); + let var2_neg = FixedTrait::new(2 * ONE_u128, true); + let var3_neg = FixedTrait::new(3 * ONE_u128, true); + let var4_pos = FixedTrait::new(4 * ONE_u128, false); + + let vec2a = Vec2Trait::new(var1_pos, var2_neg); + let vec2b = Vec2Trait::new(var3_neg, var4_pos); + + let vec2 = vec2a.dot_into_vec(vec2b); + assert(vec2.x.mag == 11 * ONE_u128, 'invalid x.mag'); + assert(vec2.x.sign == true, 'invalid x.sign'); + assert(vec2.y.mag == 11 * ONE_u128, 'invalid y.mag'); + assert(vec2.y.sign == true, 'invalid y.sign'); + + let vec2 = Vec2Trait::dot_into_vec(vec2a, vec2b); // alt syntax + assert(vec2.x.mag == 11 * ONE_u128, 'invalid x.mag'); + assert(vec2.x.sign == true, 'invalid x.sign'); + assert(vec2.y.mag == 11 * ONE_u128, 'invalid y.mag'); + assert(vec2.y.sign == true, 'invalid y.sign'); + } + + #[test] + #[available_gas(2000000)] + fn test_xx() { + let var1_pos = FixedTrait::new(ONE_u128, false); + let var2_neg = FixedTrait::new(2 * ONE_u128, true); + let vec2 = Vec2Trait::new(var1_pos, var2_neg); + + let vec2xx = vec2.xx(); + assert(vec2xx.x.mag == ONE_u128, 'invalid x.mag'); + assert(vec2xx.x.sign == false, 'invalid x.sign'); + assert(vec2xx.y.mag == ONE_u128, 'invalid y.mag'); + assert(vec2xx.y.sign == false, 'invalid y.sign'); + } + + #[test] + #[available_gas(2000000)] + fn test_xy() { + let var1_pos = FixedTrait::new(ONE_u128, false); + let var2_neg = FixedTrait::new(2 * ONE_u128, true); + let vec2 = Vec2Trait::new(var1_pos, var2_neg); + + let vec2xy = vec2.xy(); + assert(vec2xy.x.mag == ONE_u128, 'invalid x.mag'); + assert(vec2xy.x.sign == false, 'invalid x.sign'); + assert(vec2xy.y.mag == 2 * ONE_u128, 'invalid xy.mag'); + assert(vec2xy.y.sign == true, 'invalid y.sign'); + } + + #[test] + #[available_gas(2000000)] + fn test_yx() { + let var1_pos = FixedTrait::new(ONE_u128, false); + let var2_neg = FixedTrait::new(2 * ONE_u128, true); + let vec2 = Vec2Trait::new(var1_pos, var2_neg); + + let vec2yx = vec2.yx(); + assert(vec2yx.x.mag == 2 * ONE_u128, 'invalid x.mag'); + assert(vec2yx.x.sign == true, 'invalid x.sign'); + assert(vec2yx.y.mag == ONE_u128, 'invalid y.mag'); + assert(vec2yx.y.sign == false, 'invalid y.sign'); + } + + #[test] + #[available_gas(2000000)] + fn test_yy() { + let var1_pos = FixedTrait::new(ONE_u128, false); + let var2_neg = FixedTrait::new(2 * ONE_u128, true); + let vec2 = Vec2Trait::new(var1_pos, var2_neg); + + let vec2yy = vec2.yy(); + assert(vec2yy.x.mag == 2 * ONE_u128, 'invalid x.mag'); + assert(vec2yy.x.sign == true, 'invalid x.sign'); + 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 new file mode 100644 index 00000000..63ddb1e0 --- /dev/null +++ b/crates/algebra/src/vector.cairo @@ -0,0 +1,130 @@ +#[derive(Copy, Drop)] +struct Vector { + data: Span, +} + +mod errors { + const INVALID_INDEX: felt252 = 'Vector: index out of bounds'; + const INVALID_SIZE: felt252 = 'Vector: invalid size'; +} + +trait VectorTrait { + fn new(data: Span) -> Vector; + + fn get(ref self: Vector, index: u8) -> T; + + fn size(self: Vector) -> u32; + + fn dot(self: Vector, vector: Vector) -> T; +} + +impl VectorImpl, +AddEq, +Zeroable, +Copy, +Drop,> of VectorTrait { + fn new(data: Span) -> Vector { + Vector { data } + } + + fn get(ref self: Vector, index: u8) -> T { + *self.data.get(index.into()).expect(errors::INVALID_INDEX).unbox() + } + + fn size(self: Vector) -> u32 { + self.data.len() + } + + fn dot(mut self: Vector, mut vector: Vector) -> T { + // [Check] Dimesions are compatible + assert(self.size() == vector.size(), errors::INVALID_SIZE); + // [Compute] Dot product in a loop + let mut index = 0; + let mut value = Zeroable::zero(); + loop { + match self.data.pop_front() { + Option::Some(x_value) => { + let y_value = vector.data.pop_front().unwrap(); + value += *x_value * *y_value; + }, + Option::None => { break value; }, + }; + } + } +} + +impl VectorAdd< + T, +Mul, +AddEq, +Add, +Zeroable, +Copy, +Drop, +> of Add> { + fn add(mut lhs: Vector, mut rhs: Vector) -> Vector { + // [Check] Dimesions are compatible + assert(lhs.size() == rhs.size(), errors::INVALID_SIZE); + let mut values = array![]; + let max_index = lhs.size(); + let mut index: u8 = 0; + loop { + if max_index == index.into() { + break; + } + values.append(lhs.get(index) + rhs.get(index)); + index += 1; + }; + VectorTrait::new(values.span()) + } +} + +impl VectorSub< + T, +Mul, +AddEq, +Sub, +Zeroable, +Copy, +Drop, +> of Sub> { + fn sub(mut lhs: Vector, mut rhs: Vector) -> Vector { + // [Check] Dimesions are compatible + assert(lhs.size() == rhs.size(), errors::INVALID_SIZE); + let mut values = array![]; + let max_index = lhs.size(); + let mut index: u8 = 0; + loop { + if max_index == index.into() { + break; + } + values.append(lhs.get(index) - rhs.get(index)); + index += 1; + }; + VectorTrait::new(values.span()) + } +} + +#[cfg(test)] +mod tests { + // Core imports + + use debug::PrintTrait; + + // Local imports + + use super::{Vector, VectorTrait}; + + impl I128Zeroable of Zeroable { + fn zero() -> i128 { + 0 + } + fn is_zero(self: i128) -> bool { + self == 0 + } + fn is_non_zero(self: i128) -> bool { + self != 0 + } + } + + #[test] + #[available_gas(1_000_000)] + fn test_vector_get() { + let mut vector: Vector = VectorTrait::new(array![1, 2, 3, 4].span()); + assert(vector.get(0) == 1, 'Vector: get failed'); + assert(vector.get(2) == 3, 'Vector: get failed'); + } + + #[test] + #[available_gas(1_000_000)] + fn test_vector_dot_product() { + let vector1: Vector = VectorTrait::new(array![1, 2, 3].span()); + let vector2: Vector = VectorTrait::new(array![4, 5, 6].span()); + 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/crates/defi/.gitignore b/crates/defi/.gitignore new file mode 100644 index 00000000..1de56593 --- /dev/null +++ b/crates/defi/.gitignore @@ -0,0 +1 @@ +target \ No newline at end of file diff --git a/crates/defi/README.md b/crates/defi/README.md new file mode 100644 index 00000000..d806f17c --- /dev/null +++ b/crates/defi/README.md @@ -0,0 +1,173 @@ +# Gradual Dutch Auctions (GDA) + +## Introduction + +Gradual Dutch Auctions (GDA) enable efficient sales of assets without relying on liquid markets. GDAs offer a novel solution for selling both non-fungible tokens (NFTs) and fungible tokens through discrete and continuous mechanisms. + +## Discrete GDA + +### Motivation + +Discrete GDAs are perfect for selling NFTs in integer quantities. They offer an efficient way to conduct bulk purchases through a sequence of Dutch auctions. + +### Mechanism + +The process involves holding virtual Dutch auctions for each token, allowing for efficient clearing of batches. Price decay is exponential, controlled by a decay constant, and the starting price increases by a fixed scale factor. + +### Calculating Batch Purchase Prices + +Calculations can be made efficiently for purchasing a batch of auctions, following a given price function. + +## Continuous GDA + +### Motivation + +Continuous GDAs offer a mechanism for selling fungible tokens, allowing for constant rate emissions over time. + +### Mechanism + +The process works by incrementally making more assets available for sale, splitting sales into an infinite sequence of auctions. Various price functions, including exponential decay, can be applied. + +### Calculating Purchase Prices + +It's possible to compute the purchase price for any quantity of tokens gas-efficiently, using specific mathematical expressions. + +## How to Use + +### Discrete Gradual Dutch Auction + +The `DiscreteGDA` structure represents a Gradual Dutch Auction using discrete time steps. Here's how you can use it: + +#### Creating a Discrete GDA + +```rust +let gda = DiscreteGDA { + sold: Fixed::new_unscaled(0), + initial_price: Fixed::new_unscaled(100, false), + scale_factor: FixedTrait::new_unscaled(11, false) / FixedTrait::new_unscaled(10, false), // 1.1 + decay_constant: FixedTrait::new_unscaled(1, false) / FixedTrait::new_unscaled(2, false), // 0.5, +}; +``` + +#### Calculating the Purchase Price + +You can calculate the purchase price for a specific quantity at a given time using the `purchase_price` method. + +```rust +let time_since_start = FixedTrait::new(2, false); // 2 days since the start, it must be scaled to avoid overflow. +let quantity = FixedTrait::new_unscaled(5, false); // Quantity to purchase +let price = gda.purchase_price(time_since_start, quantity); +``` + +### Continuous Gradual Dutch Auction + +The `ContinuousGDA` structure represents a Gradual Dutch Auction using continuous time steps. + +#### Creating a Continuous GDA + +```rust +let gda = ContinuousGDA { + initial_price: FixedTrait::new_unscaled(1000, false), + emission_rate: FixedTrait::ONE(), + decay_constant: FixedTrait::new_unscaled(1, false) / FixedTrait::new_unscaled(2, false), +}; +``` + +#### Calculating the Purchase Price + +Just like with the discrete version, you can calculate the purchase price for a specific quantity at a given time using the `purchase_price` method. + +```rust +let time_since_last = FixedTrait::new(1, false); // 1 day since the last purchase, it must be scaled to avoid overflow. +let quantity = FixedTrait::new_unscaled(3, false); // Quantity to purchase +let price = gda.purchase_price(time_since_last, quantity); +``` + +--- + +These examples demonstrate how to create instances of the `DiscreteGDA` and `ContinuousGDA` structures, and how to utilize their `purchase_price` methods to calculate the price for purchasing specific quantities at given times. + +You'll need to include the `cubit` crate in your project to work with the `Fixed` type and mathematical operations like `exp` and `pow`. Make sure to follow the respective documentation for additional details and proper integration into your project. + +## Conclusion + +GDAs present a powerful tool for selling both fungible and non-fungible tokens in various contexts. They offer efficient, flexible solutions for asset sales, opening doors to innovative applications beyond traditional markets. + +# Variable Rate GDAs (VRGDAs) + +## Overview + +Variable Rate GDAs (VRGDAs) enable the selling of tokens according to a custom schedule, raising or lowering prices based on the sales pace. VRGDA is a generalization of the GDA mechanism. + +## How to Use + +### Linear Variable Rate Gradual Dutch Auction (LinearVRGDA) + +The `LinearVRGDA` struct represents a linear auction where the price decays based on the target price, decay constant, and per-time-unit rate. + +#### Creating a LinearVRGDA instance + +```rust +const _69_42: u128 = 1280572973596917000000; +const _0_31: u128 = 5718490662849961000; + +let auction = LinearVRGDA { + target_price: FixedTrait::new(_69_42, false), + decay_constant: FixedTrait::new(_0_31, false), + per_time_unit: FixedTrait::new_unscaled(2, false), +}; +``` + +#### Calculating Target Sale Time + +```rust +let target_sale_time = auction.get_target_sale_time(sold_quantity); +``` + +#### Calculating VRGDA Price + +```rust +let price = auction.get_vrgda_price(time_since_start, sold_quantity); +``` + +### Logistic Variable Rate Gradual Dutch Auction (LogisticVRGDA) + +The `LogisticVRGDA` struct represents an auction where the price decays according to a logistic function, based on the target price, decay constant, max sellable quantity, and time scale. + +#### Creating a LogisticVRGDA instance + +```rust +const MAX_SELLABLE: u128 = 6392; +const _0_0023: u128 = 42427511369531970; + +let auction = LogisticVRGDA { + target_price: FixedTrait::new(_69_42, false), + decay_constant: FixedTrait::new(_0_31, false), + max_sellable: FixedTrait::new_unscaled(MAX_SELLABLE, false), + time_scale: FixedTrait::new(_0_0023, false), +}; +``` + +#### Calculating Target Sale Time + +```rust +let target_sale_time = auction.get_target_sale_time(sold_quantity); +``` + +#### Calculating VRGDA Price + +```rust +let price = auction.get_vrgda_price(time_since_start, sold_quantity); +``` + +Make sure to import the required dependencies at the beginning of your Cairo file: + +```rust +use cubit::f128::types::fixed::{Fixed, FixedTrait}; +``` + +These examples show you how to create instances of both `LinearVRGDA` and `LogisticVRGDA` and how to use their methods to calculate the target sale time and VRGDA price. + +## Conclusion + +VRGDAs offer a flexible way to issue NFTs on nearly any schedule, enabling seamless purchases at any time. diff --git a/crates/defi/Scarb.lock b/crates/defi/Scarb.lock new file mode 100644 index 00000000..0267575e --- /dev/null +++ b/crates/defi/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/ponderingdemocritus/cubit?rev=9c3bbdf#9c3bbdfee7b165ab06ac8cc7046ce4d4c8866bfc" + +[[package]] +name = "defi" +version = "0.0.0" +dependencies = [ + "cubit", + "dojo", +] + +[[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" diff --git a/crates/defi/Scarb.toml b/crates/defi/Scarb.toml new file mode 100644 index 00000000..fa9773d6 --- /dev/null +++ b/crates/defi/Scarb.toml @@ -0,0 +1,8 @@ +[package] +name = "defi" +version = "0.0.0" +description = "Implementations of defi primitives" +homepage = "https://github.com/dojoengine/origami/tree/crates/defi" + +[dependencies] +cubit = { git = "https://github.com/influenceth/cubit", rev = "b459053" } \ No newline at end of file diff --git a/crates/defi/src/auction/gda.cairo b/crates/defi/src/auction/gda.cairo new file mode 100644 index 00000000..d375680a --- /dev/null +++ b/crates/defi/src/auction/gda.cairo @@ -0,0 +1,259 @@ +// External imports + +use cubit::f128::math::core::{exp, pow}; +use cubit::f128::types::fixed::{Fixed, FixedTrait}; + +/// A Gradual Dutch Auction represented using discrete time steps. +/// The purchase price for a given quantity is calculated based on +/// the initial price, scale factor, decay constant, and the time since +/// the auction has started. +#[derive(Copy, Drop, Serde, starknet::Storage)] +struct DiscreteGDA { + sold: Fixed, + initial_price: Fixed, + scale_factor: Fixed, + decay_constant: Fixed, +} + +#[generate_trait] +impl DiscreteGDAImpl of DiscreteGDATrait { + /// Calculates the purchase price for a given quantity of the item at a specific time. + /// + /// # Arguments + /// + /// * `time_since_start`: Time since the start of the auction in days. + /// * `quantity`: Quantity of the item being purchased. + /// + /// # Returns + /// + /// * A `Fixed` representing the purchase price. + fn purchase_price(self: @DiscreteGDA, time_since_start: Fixed, quantity: Fixed) -> Fixed { + let num1 = *self.initial_price * pow(*self.scale_factor, *self.sold); + let num2 = pow(*self.scale_factor, quantity) - FixedTrait::ONE(); + let den1 = exp(*self.decay_constant * time_since_start); + let den2 = *self.scale_factor - FixedTrait::ONE(); + (num1 * num2) / (den1 * den2) + } +} + +/// A Gradual Dutch Auction represented using continuous time steps. +/// The purchase price is calculated based on the initial price, +/// emission rate, decay constant, and the time since the last purchase in days. +#[derive(Copy, Drop, Serde, starknet::Storage)] +struct ContinuousGDA { + initial_price: Fixed, + emission_rate: Fixed, + decay_constant: Fixed, +} + +#[generate_trait] +impl ContinuousGDAImpl of ContinuousGDATrait { + /// Calculates the purchase price for a given quantity of the item at a specific time. + /// + /// # Arguments + /// + /// * `time_since_last`: Time since the last purchase in the auction in days. + /// * `quantity`: Quantity of the item being purchased. + /// + /// # Returns + /// + /// * A `Fixed` representing the purchase price. + fn purchase_price(self: @ContinuousGDA, time_since_last: Fixed, quantity: Fixed) -> Fixed { + let num1 = *self.initial_price / *self.decay_constant; + let num2 = exp((*self.decay_constant * quantity) / *self.emission_rate) - FixedTrait::ONE(); + let den = exp(*self.decay_constant * time_since_last); + (num1 * num2) / den + } +} + +#[cfg(test)] +mod tests { + // External imports + + use cubit::f128::types::fixed::{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'); + } + + mod continuous { + // Local imports + + use super::{Fixed, FixedTrait}; + use super::{assert_approx_equal, TOLERANCE}; + use super::super::{ContinuousGDA, ContinuousGDATrait}; + + // ipynb with calculations at https://colab.research.google.com/drive/14elIFRXdG3_gyiI43tP47lUC_aClDHfB?usp=sharing + #[test] + #[available_gas(2000000)] + fn test_price_1() { + let auction = ContinuousGDA { + initial_price: FixedTrait::new_unscaled(1000, false), + emission_rate: FixedTrait::ONE(), + decay_constant: FixedTrait::new_unscaled(1, false) + / FixedTrait::new_unscaled(2, false), + }; + let expected = FixedTrait::new(22128445337405634000000, false); + let time_since_last = FixedTrait::new_unscaled(10, false); + let quantity = FixedTrait::new_unscaled(9, false); + let price: Fixed = auction.purchase_price(time_since_last, quantity); + assert_approx_equal(price, expected, TOLERANCE) + } + + + #[test] + #[available_gas(2000000)] + fn test_price_2() { + let auction = ContinuousGDA { + initial_price: FixedTrait::new_unscaled(1000, false), + emission_rate: FixedTrait::ONE(), + decay_constant: FixedTrait::new_unscaled(1, false) + / FixedTrait::new_unscaled(2, false), + }; + let expected = FixedTrait::new(89774852279643700000, false); + let time_since_last = FixedTrait::new_unscaled(20, false); + let quantity = FixedTrait::new_unscaled(8, false); + let price: Fixed = auction.purchase_price(time_since_last, quantity); + assert_approx_equal(price, expected, TOLERANCE) + } + + #[test] + #[available_gas(2000000)] + fn test_price_3() { + let auction = ContinuousGDA { + initial_price: FixedTrait::new_unscaled(1000, false), + emission_rate: FixedTrait::ONE(), + decay_constant: FixedTrait::new_unscaled(1, false) + / FixedTrait::new_unscaled(2, false), + }; + let expected = FixedTrait::new(20393925850936156000, false); + let time_since_last = FixedTrait::new_unscaled(30, false); + let quantity = FixedTrait::new_unscaled(15, false); + let price: Fixed = auction.purchase_price(time_since_last, quantity); + assert_approx_equal(price, expected, TOLERANCE) + } + + #[test] + #[available_gas(2000000)] + fn test_price_4() { + let auction = ContinuousGDA { + initial_price: FixedTrait::new_unscaled(1000, false), + emission_rate: FixedTrait::ONE(), + decay_constant: FixedTrait::new_unscaled(1, false) + / FixedTrait::new_unscaled(2, false), + }; + let expected = FixedTrait::new(3028401847768577000000, false); + let time_since_last = FixedTrait::new_unscaled(40, false); + let quantity = FixedTrait::new_unscaled(35, false); + let price: Fixed = auction.purchase_price(time_since_last, quantity); + assert_approx_equal(price, expected, TOLERANCE) + } + } + + mod discrete { + // Local imports + + use super::{Fixed, FixedTrait}; + use super::{assert_approx_equal, TOLERANCE}; + use super::super::{DiscreteGDA, DiscreteGDATrait}; + + #[test] + #[available_gas(2000000)] + fn test_initial_price() { + let auction = DiscreteGDA { + sold: FixedTrait::new_unscaled(0, false), + initial_price: FixedTrait::new_unscaled(1000, false), + scale_factor: FixedTrait::new_unscaled(11, false) + / FixedTrait::new_unscaled(10, false), + decay_constant: FixedTrait::new_unscaled(1, false) + / FixedTrait::new_unscaled(2, false), + }; + let price = auction.purchase_price(FixedTrait::ZERO(), FixedTrait::ONE()); + assert_approx_equal(price, auction.initial_price, TOLERANCE) + } + + // ipynb with calculations at https://colab.research.google.com/drive/14elIFRXdG3_gyiI43tP47lUC_aClDHfB?usp=sharing + #[test] + #[available_gas(2000000)] + fn test_price_1() { + let auction = DiscreteGDA { + sold: FixedTrait::new_unscaled(1, false), + initial_price: FixedTrait::new_unscaled(1000, false), + scale_factor: FixedTrait::new_unscaled(11, false) + / FixedTrait::new_unscaled(10, false), + decay_constant: FixedTrait::new_unscaled(1, false) + / FixedTrait::new_unscaled(2, false), + }; + let expected = FixedTrait::new(1856620062541316600000, false); + let price = auction + .purchase_price( + FixedTrait::new_unscaled(10, false), FixedTrait::new_unscaled(9, false), + ); + assert_approx_equal(price, expected, TOLERANCE) + } + + #[test] + #[available_gas(2000000)] + fn test_price_2() { + let auction = DiscreteGDA { + sold: FixedTrait::new_unscaled(2, false), + initial_price: FixedTrait::new_unscaled(1000, false), + scale_factor: FixedTrait::new_unscaled(11, false) + / FixedTrait::new_unscaled(10, false), + decay_constant: FixedTrait::new(1, false) / FixedTrait::new(2, false), + }; + let expected = FixedTrait::new(2042282068795448600000, false); + let price = auction + .purchase_price( + FixedTrait::new_unscaled(10, false), FixedTrait::new_unscaled(9, false), + ); + assert_approx_equal(price, expected, TOLERANCE) + } + + #[test] + #[available_gas(2000000)] + fn test_price_3() { + let auction = DiscreteGDA { + sold: FixedTrait::new_unscaled(4, false), + initial_price: FixedTrait::new_unscaled(1000, false), + scale_factor: FixedTrait::new_unscaled(11, false) + / FixedTrait::new_unscaled(10, false), + decay_constant: FixedTrait::new_unscaled(1, false) + / FixedTrait::new_unscaled(2, false), + }; + let expected = FixedTrait::new(2471161303242493000000, false); + let price = auction + .purchase_price( + FixedTrait::new_unscaled(10, false), FixedTrait::new_unscaled(9, false), + ); + assert_approx_equal(price, expected, TOLERANCE) + } + + #[test] + #[available_gas(2000000)] + fn test_price_4() { + let auction = DiscreteGDA { + sold: FixedTrait::new_unscaled(20, false), + initial_price: FixedTrait::new_unscaled(1000, false), + scale_factor: FixedTrait::new_unscaled(11, false) + / FixedTrait::new_unscaled(10, false), + decay_constant: FixedTrait::new_unscaled(1, false) + / FixedTrait::new_unscaled(2, false), + }; + let expected = FixedTrait::new(291, false); + let price = auction + .purchase_price( + FixedTrait::new_unscaled(85, false), FixedTrait::new_unscaled(1, false), + ); + assert_approx_equal(price, expected, TOLERANCE) + } + } +} diff --git a/crates/defi/src/auction/helpers.cairo b/crates/defi/src/auction/helpers.cairo new file mode 100644 index 00000000..dec1ec70 --- /dev/null +++ b/crates/defi/src/auction/helpers.cairo @@ -0,0 +1,38 @@ +// External imports + +use cubit::f128::types::fixed::{Fixed, FixedTrait}; + +fn to_days_fp(x: Fixed) -> Fixed { + x / FixedTrait::new(86400, false) +} + +fn from_days_fp(x: Fixed) -> Fixed { + x * FixedTrait::new(86400, false) +} + +#[cfg(test)] +mod tests { + // External imports + + use cubit::f128::types::fixed::{Fixed, FixedTrait}; + + // Local imports + + use super::{to_days_fp, from_days_fp}; + + // Constants + + const TOLERANCE: u128 = 18446744073709550; // 0.001 + + #[test] + #[available_gas(20000000)] + fn test_days_convertions() { + let days = FixedTrait::new(2, false); + let actual = to_days_fp(from_days_fp(days)); + let tolerance = TOLERANCE * 10; + let left_bound = days - FixedTrait::new(tolerance, false); + let right_bound = days + FixedTrait::new(tolerance, false); + assert(left_bound <= actual && actual <= right_bound, 'Not approx eq'); + } +} + diff --git a/crates/defi/src/auction/vrgda.cairo b/crates/defi/src/auction/vrgda.cairo new file mode 100644 index 00000000..c4557e02 --- /dev/null +++ b/crates/defi/src/auction/vrgda.cairo @@ -0,0 +1,279 @@ +// External imports + +use cubit::f128::math::core::{ln, abs, exp}; +use cubit::f128::types::fixed::{Fixed, FixedTrait}; + +/// A Linear Variable Rate Gradual Dutch Auction (VRGDA) struct. +/// Represents an auction where the price decays linearly based on the target price, +/// decay constant, and per-time-unit rate. +#[derive(Copy, Drop, Serde, starknet::Storage)] +struct LinearVRGDA { + target_price: Fixed, + decay_constant: Fixed, + per_time_unit: Fixed, +} + +#[generate_trait] +impl LinearVRGDAImpl of LinearVRGDATrait { + /// Calculates the target sale time based on the quantity sold. + /// + /// # Arguments + /// + /// * `sold`: Quantity sold. + /// + /// # Returns + /// + /// * A `Fixed` representing the target sale time. + fn get_target_sale_time(self: @LinearVRGDA, sold: Fixed) -> Fixed { + sold / *self.per_time_unit + } + + /// Calculates the VRGDA price at a specific time since the auction started. + /// + /// # Arguments + /// + /// * `time_since_start`: Time since the auction started. + /// * `sold`: Quantity sold. + /// + /// # Returns + /// + /// * A `Fixed` representing the price. + fn get_vrgda_price(self: @LinearVRGDA, time_since_start: Fixed, sold: Fixed) -> Fixed { + *self.target_price + * exp( + *self.decay_constant + * (time_since_start + - self.get_target_sale_time(sold + FixedTrait::new(1, false))) + ) + } + + fn get_reverse_vrgda_price(self: @LinearVRGDA, time_since_start: Fixed, sold: Fixed) -> Fixed { + *self.target_price + * exp( + (*self.decay_constant * FixedTrait::new(1, true)) + * (time_since_start + - self.get_target_sale_time(sold + FixedTrait::new(1, false))) + ) + } +} + +#[derive(Copy, Drop, Serde, starknet::Storage)] +struct LogisticVRGDA { + target_price: Fixed, + decay_constant: Fixed, + max_sellable: Fixed, + time_scale: Fixed, +} + +// A Logistic Variable Rate Gradual Dutch Auction (VRGDA) struct. +/// Represents an auction where the price decays according to a logistic function, +/// based on the target price, decay constant, max sellable quantity, and time scale. +#[generate_trait] +impl LogisticVRGDAImpl of LogisticVRGDATrait { + /// Calculates the target sale time using a logistic function based on the quantity sold. + /// + /// # Arguments + /// + /// * `sold`: Quantity sold. + /// + /// # Returns + /// + /// * A `Fixed` representing the target sale time. + fn get_target_sale_time(self: @LogisticVRGDA, sold: Fixed) -> Fixed { + let logistic_limit = *self.max_sellable + FixedTrait::ONE(); + let logistic_limit_double = logistic_limit * FixedTrait::new_unscaled(2, false); + abs( + ln(logistic_limit_double / (sold + logistic_limit) - FixedTrait::ONE()) + / *self.time_scale + ) + } + + /// Calculates the VRGDA price at a specific time since the auction started, + /// using the logistic function. + /// + /// # Arguments + /// + /// * `time_since_start`: Time since the auction started. + /// * `sold`: Quantity sold. + /// + /// # Returns + /// + /// * A `Fixed` representing the price. + fn get_vrgda_price(self: @LogisticVRGDA, time_since_start: Fixed, sold: Fixed) -> Fixed { + *self.target_price + * exp( + *self.decay_constant + * (time_since_start + - self.get_target_sale_time(sold + FixedTrait::new(1, false))) + ) + } + + fn get_reverse_vrgda_price( + self: @LogisticVRGDA, time_since_start: Fixed, sold: Fixed + ) -> Fixed { + *self.target_price + * exp( + (*self.decay_constant * FixedTrait::new(1, true)) + * (time_since_start + - self.get_target_sale_time(sold + FixedTrait::new(1, false))) + ) + } +} + +#[cfg(test)] +mod tests { + // External imports + + use cubit::f128::types::fixed::{Fixed, FixedTrait}; + + // Constants + + // Helpers + + fn to_days_fp(x: Fixed) -> Fixed { + x / FixedTrait::new(86400, false) + } + + fn from_days_fp(x: Fixed) -> Fixed { + x * FixedTrait::new(86400, false) + } + + fn assert_rel_approx_eq(a: Fixed, b: Fixed, max_percent_delta: Fixed) { + if b == FixedTrait::ZERO() { + assert(a == b, 'a should eq ZERO'); + } + let percent_delta = if a > b { + (a - b) / b + } else { + (b - a) / b + }; + + assert(percent_delta < max_percent_delta, 'a ~= b not satisfied'); + } + + mod linear { + // Local imports + + use super::{Fixed, FixedTrait}; + use super::{to_days_fp, from_days_fp}; + use super::assert_rel_approx_eq; + use super::super::{LinearVRGDA, LinearVRGDATrait}; + + // Constants + + const _69_42: u128 = 1280572973596917000000; + const _0_31: u128 = 5718490662849961000; + const DELTA_0_0005: u128 = 9223372036854776; + const DELTA_0_02: u128 = 368934881474191000; + const DELTA: u128 = 184467440737095; + + #[test] + #[available_gas(2000000)] + fn test_target_price() { + let auction = LinearVRGDA { + target_price: FixedTrait::new(_69_42, false), + decay_constant: FixedTrait::new(_0_31, false), + per_time_unit: FixedTrait::new_unscaled(2, false), + }; + let time = from_days_fp(auction.get_target_sale_time(FixedTrait::new(1, false))); + let cost = auction + .get_vrgda_price(to_days_fp(time + FixedTrait::new(1, false)), FixedTrait::ZERO()); + assert_rel_approx_eq(cost, auction.target_price, FixedTrait::new(DELTA_0_0005, false)); + } + + #[test] + #[available_gas(20000000)] + fn test_pricing_basic() { + let auction = LinearVRGDA { + target_price: FixedTrait::new(_69_42, false), + decay_constant: FixedTrait::new(_0_31, false), + per_time_unit: FixedTrait::new_unscaled(2, false), + }; + let time_delta = FixedTrait::new(10368001, false); // 120 days + let num_mint = FixedTrait::new(239, true); + let cost = auction.get_vrgda_price(time_delta, num_mint); + assert_rel_approx_eq(cost, auction.target_price, FixedTrait::new(DELTA_0_02, false)); + } + + #[test] + #[available_gas(20000000)] + fn test_pricing_basic_reverse() { + let auction = LinearVRGDA { + target_price: FixedTrait::new(_69_42, false), + decay_constant: FixedTrait::new(_0_31, false), + per_time_unit: FixedTrait::new_unscaled(2, false), + }; + let time_delta = FixedTrait::new(10368001, false); // 120 days + let num_mint = FixedTrait::new(239, true); + let cost = auction.get_reverse_vrgda_price(time_delta, num_mint); + assert_rel_approx_eq(cost, auction.target_price, FixedTrait::new(DELTA_0_02, false)); + } + } + + mod logistic { + // Local imports + + use super::{Fixed, FixedTrait}; + use super::{to_days_fp, from_days_fp}; + use super::assert_rel_approx_eq; + use super::super::{LogisticVRGDA, LogisticVRGDATrait}; + + // Constants + + const _69_42: u128 = 1280572973596917000000; + const _0_31: u128 = 5718490662849961000; + const DELTA_0_0005: u128 = 9223372036854776; + const DELTA_0_02: u128 = 368934881474191000; + const MAX_SELLABLE: u128 = 6392; + const _0_0023: u128 = 42427511369531970; + + #[test] + #[available_gas(200000000)] + fn test_target_price() { + let auction = LogisticVRGDA { + target_price: FixedTrait::new(_69_42, false), + decay_constant: FixedTrait::new(_0_31, false), + max_sellable: FixedTrait::new_unscaled(MAX_SELLABLE, false), + time_scale: FixedTrait::new(_0_0023, false), + }; + let time = from_days_fp(auction.get_target_sale_time(FixedTrait::new(1, false))); + + let cost = auction + .get_vrgda_price(time + FixedTrait::new(1, false), FixedTrait::ZERO()); + assert_rel_approx_eq(cost, auction.target_price, FixedTrait::new(DELTA_0_0005, false)); + } + + #[test] + #[available_gas(200000000)] + fn test_pricing_basic() { + let auction = LogisticVRGDA { + target_price: FixedTrait::new(_69_42, false), + decay_constant: FixedTrait::new(_0_31, false), + max_sellable: FixedTrait::new_unscaled(MAX_SELLABLE, false), + time_scale: FixedTrait::new(_0_0023, false), + }; + let time_delta = FixedTrait::new(10368001, false); + let num_mint = FixedTrait::new(876, false); + + let cost = auction.get_vrgda_price(time_delta, num_mint); + assert_rel_approx_eq(cost, auction.target_price, FixedTrait::new(DELTA_0_02, false)); + } + + #[test] + #[available_gas(200000000)] + fn test_pricing_basic_reverse() { + let auction = LogisticVRGDA { + target_price: FixedTrait::new(_69_42, false), + decay_constant: FixedTrait::new(_0_31, false), + max_sellable: FixedTrait::new_unscaled(MAX_SELLABLE, false), + time_scale: FixedTrait::new(_0_0023, false), + }; + let time_delta = FixedTrait::new(10368001, false); + let num_mint = FixedTrait::new(876, false); + + let cost = auction.get_reverse_vrgda_price(time_delta, num_mint); + assert_rel_approx_eq(cost, auction.target_price, FixedTrait::new(DELTA_0_02, false)); + } + } +} + diff --git a/crates/defi/src/lib.cairo b/crates/defi/src/lib.cairo new file mode 100644 index 00000000..3cacd2f0 --- /dev/null +++ b/crates/defi/src/lib.cairo @@ -0,0 +1,6 @@ +mod auction { + mod gda; + mod vrgda; + mod helpers; +} + diff --git a/crates/random/Scarb.toml b/crates/random/Scarb.toml new file mode 100644 index 00000000..785011c2 --- /dev/null +++ b/crates/random/Scarb.toml @@ -0,0 +1,8 @@ +[package] +name = "random" +version = "0.0.0" +description = "A set of random mechanism for games" +homepage = "https://github.com/dojoengine/origami/tree/main/crates/random" + +[dependencies] +dojo.workspace = true \ No newline at end of file diff --git a/crates/random/src/deck.cairo b/crates/random/src/deck.cairo new file mode 100644 index 00000000..8e11ba6f --- /dev/null +++ b/crates/random/src/deck.cairo @@ -0,0 +1,205 @@ +//! Deck struct and random card drawing methods. + +// Core imports + +use dict::{Felt252Dict, Felt252DictTrait}; +use hash::HashStateTrait; +use poseidon::PoseidonTrait; +use traits::{Into, Drop}; + +/// Deck struct. +#[derive(Destruct)] +struct Deck { + seed: felt252, + keys: Felt252Dict, + cards: Felt252Dict, + remaining: u32, + nonce: u8, +} + +/// Errors module. +mod errors { + const NO_CARD_LEFT: felt252 = 'Deck: no card left'; +} + +/// Trait to initialize, draw and discard a card from the Deck. +trait DeckTrait { + /// Returns a new `Deck` struct. + /// # Arguments + /// * `seed` - A seed to initialize the deck. + /// * `number` - The initial number of cards. + /// # Returns + /// * The initialized `Deck`. + fn new(seed: felt252, number: u32) -> Deck; + /// Returns a card type after a draw. + /// # Arguments + /// * `self` - The Deck. + /// # Returns + /// * The card type. + fn draw(ref self: Deck) -> u8; + /// Returns a card into the deck, the card becomes drawable. + /// # Arguments + /// * `self` - The Deck. + /// * `card` - The card to discard. + fn discard(ref self: Deck, card: u8); + /// Withdraw a card from the deck, the card is not drawable anymore. + /// # Arguments + /// * `self` - The Deck. + /// * `card` - The card to withdraw. + fn withdraw(ref self: Deck, card: u8); + /// Remove the cards from the deck, they are not drawable anymore. + /// # Arguments + /// * `self` - The Deck. + /// * `cards` - The card to set. + fn remove(ref self: Deck, cards: Span); +} + +/// Implementation of the `DeckTrait` trait for the `Deck` struct. +impl DeckImpl of DeckTrait { + #[inline(always)] + fn new(seed: felt252, number: u32) -> Deck { + Deck { + seed, cards: Default::default(), keys: Default::default(), remaining: number, nonce: 0 + } + } + + #[inline(always)] + fn draw(ref self: Deck) -> u8 { + // [Check] Enough cards left. + assert(self.remaining > 0, errors::NO_CARD_LEFT); + // [Compute] Draw a random card from remainingcs cards. + let mut state = PoseidonTrait::new(); + state = state.update(self.seed); + state = state.update(self.nonce.into()); + state = state.update(self.remaining.into()); + let random: u256 = state.finalize().into(); + + let key: felt252 = (random % self.remaining.into() + 1).try_into().unwrap(); + let mut card: u8 = self.cards.get(key); + if 0 == card.into() { + card = key.try_into().unwrap(); + } + + // [Compute] Remove card from the deck. + self.withdraw(card); + self.nonce += 1; + card + } + + #[inline(always)] + fn discard(ref self: Deck, card: u8) { + self.remaining += 1; + self.cards.insert(self.remaining.into(), card); + } + + #[inline(always)] + fn withdraw(ref self: Deck, card: u8) { + let mut key = self.keys.get(card.into()); + if key == 0 { + key = card.into(); + } + let latest_key: felt252 = self.remaining.into(); + if latest_key != key { + let mut latest_card: u8 = self.cards.get(latest_key); + if latest_card == 0 { + latest_card = latest_key.try_into().unwrap(); + } + self.cards.insert(key, latest_card); + self.keys.insert(latest_card.into(), key); + } + self.remaining -= 1; + } + + fn remove(ref self: Deck, mut cards: Span) { + loop { + match cards.pop_front() { + Option::Some(card) => { self.withdraw(*card); }, + Option::None => { break; }, + }; + }; + } +} + +#[cfg(test)] +mod tests { + // Core imports + + use debug::PrintTrait; + + // Local imports + + use super::DeckTrait; + + // Constants + + const DECK_CARDS_NUMBER: u32 = 5; + const DECK_SEED: felt252 = 'SEED'; + + #[test] + #[available_gas(500_000)] + fn test_deck_new_draw() { + let mut deck = DeckTrait::new(DECK_SEED, DECK_CARDS_NUMBER); + assert(deck.remaining == DECK_CARDS_NUMBER, 'Wrong remaining'); + assert(deck.draw() == 0x2, 'Wrong card 01'); + assert(deck.draw() == 0x4, 'Wrong card 02'); + assert(deck.draw() == 0x1, 'Wrong card 03'); + assert(deck.draw() == 0x5, 'Wrong card 04'); + assert(deck.draw() == 0x3, 'Wrong card 05'); + assert(deck.remaining == 0, 'Wrong remaining'); + } + + #[test] + #[available_gas(500_000)] + fn test_deck_new_withdraw() { + let mut deck = DeckTrait::new(DECK_SEED, DECK_CARDS_NUMBER); + deck.withdraw(0x2); + assert(deck.draw() == 0x3, 'Wrong card 01'); + assert(deck.draw() == 0x1, 'Wrong card 02'); + assert(deck.draw() == 0x5, 'Wrong card 03'); + assert(deck.draw() == 0x4, 'Wrong card 04'); + assert(deck.remaining == 0, 'Wrong remaining'); + } + + #[test] + #[available_gas(100_000)] + #[should_panic(expected: ('Deck: no card left',))] + fn test_deck_new_draw_revert_no_card_left() { + let mut deck = DeckTrait::new(DECK_SEED, DECK_CARDS_NUMBER); + deck.remaining = 0; + deck.draw(); + } + + #[test] + #[available_gas(600_000)] + fn test_deck_new_discard() { + let mut deck = DeckTrait::new(DECK_SEED, DECK_CARDS_NUMBER); + loop { + if deck.remaining == 0 { + break; + }; + deck.draw(); + }; + let card: u8 = 0x11; + deck.discard(card); + assert(deck.draw() == card, 'Wrong card'); + } + + #[test] + #[available_gas(400_000)] + fn test_deck_new_remove() { + let mut deck = DeckTrait::new(DECK_SEED, DECK_CARDS_NUMBER); + let mut cards: Array = array![]; + let mut card: u8 = 1; + loop { + if card.into() > DECK_CARDS_NUMBER { + break; + }; + cards.append(card); + card += 1; + }; + deck.remove(cards.span()); + let card: u8 = 0x11; + deck.discard(card); + assert(deck.draw() == card, 'Wrong card'); + } +} diff --git a/crates/random/src/dice.cairo b/crates/random/src/dice.cairo new file mode 100644 index 00000000..bc2f8eb4 --- /dev/null +++ b/crates/random/src/dice.cairo @@ -0,0 +1,87 @@ +//! Dice struct and methods for random dice rolls. + +// Core imports + +use poseidon::PoseidonTrait; +use hash::HashStateTrait; +use traits::Into; + +/// Dice struct. +#[derive(Drop)] +struct Dice { + face_count: u8, + seed: felt252, + nonce: felt252, +} + +/// Trait to initialize and roll a dice. +trait DiceTrait { + /// Returns a new `Dice` struct. + /// # Arguments + /// * `face_count` - The number of faces. + /// * `seed` - A seed to initialize the dice. + /// # Returns + /// * The initialized `Dice`. + fn new(face_count: u8, seed: felt252) -> Dice; + /// Returns a value after a die roll. + /// # Arguments + /// * `self` - The Dice. + /// # Returns + /// * The value of the dice after a roll. + fn roll(ref self: Dice) -> u8; +} + +/// Implementation of the `DiceTrait` trait for the `Dice` struct. +impl DiceImpl of DiceTrait { + #[inline(always)] + fn new(face_count: u8, seed: felt252) -> Dice { + Dice { face_count, seed, nonce: 0 } + } + + #[inline(always)] + fn roll(ref self: Dice) -> u8 { + let mut state = PoseidonTrait::new(); + state = state.update(self.seed); + state = state.update(self.nonce); + self.nonce += 1; + let random: u256 = state.finalize().into(); + (random % self.face_count.into() + 1).try_into().unwrap() + } +} + +#[cfg(test)] +mod tests { + // Core imports + + use debug::PrintTrait; + + // Local imports + + use super::DiceTrait; + + // Constants + + const DICE_FACE_COUNT: u8 = 6; + const DICE_SEED: felt252 = 'SEED'; + + #[test] + #[available_gas(2000000)] + fn test_dice_new_roll() { + let mut dice = DiceTrait::new(DICE_FACE_COUNT, DICE_SEED); + assert(dice.roll() == 1, 'Wrong dice value'); + assert(dice.roll() == 6, 'Wrong dice value'); + assert(dice.roll() == 3, 'Wrong dice value'); + assert(dice.roll() == 1, 'Wrong dice value'); + assert(dice.roll() == 4, 'Wrong dice value'); + assert(dice.roll() == 3, 'Wrong dice value'); + } + + #[test] + #[available_gas(2000000)] + fn test_dice_new_roll_overflow() { + let mut dice = DiceTrait::new(DICE_FACE_COUNT, DICE_SEED); + dice.nonce = 0x800000000000011000000000000000000000000000000000000000000000000; // PRIME - 1 + dice.roll(); + assert(dice.nonce == 0, 'Wrong dice nonce'); + } +} diff --git a/crates/random/src/lib.cairo b/crates/random/src/lib.cairo new file mode 100644 index 00000000..68c584d0 --- /dev/null +++ b/crates/random/src/lib.cairo @@ -0,0 +1,2 @@ +mod deck; +mod dice; diff --git a/crates/security/Scarb.toml b/crates/security/Scarb.toml new file mode 100644 index 00000000..9a2dcb51 --- /dev/null +++ b/crates/security/Scarb.toml @@ -0,0 +1,8 @@ +[package] +name = "security" +version = "0.0.0" +description = "A set of security patterns for games" +homepage = "https://github.com/dojoengine/origami/tree/main/crates/security" + +[dependencies] +dojo.workspace = true \ No newline at end of file diff --git a/crates/security/src/commitment.cairo b/crates/security/src/commitment.cairo new file mode 100644 index 00000000..765f1020 --- /dev/null +++ b/crates/security/src/commitment.cairo @@ -0,0 +1,68 @@ +use poseidon::poseidon_hash_span; + +#[derive(Copy, Drop, Default, Serde, dojo::Introspect)] +struct Commitment { + hash: felt252 +} + +/// Errors module. +mod errors { + const COMMITMENT_INVALID_HASH: felt252 = 'Commitment: can not commit zero'; +} + +trait CommitmentTrait { + fn new() -> Commitment; + fn commit(ref self: Commitment, hash: felt252); + fn reveal, impl TDrop: Drop>(self: @Commitment, reveal: T) -> bool; +} + +impl CommitmentImpl of CommitmentTrait { + fn new() -> Commitment { + Commitment { hash: 0 } + } + + fn commit(ref self: Commitment, hash: felt252) { + assert(hash.is_non_zero(), errors::COMMITMENT_INVALID_HASH); + self.hash = hash; + } + + fn reveal, impl TDrop: Drop>(self: @Commitment, reveal: T) -> bool { + let mut serialized = array![]; + reveal.serialize(ref serialized); + let hash = poseidon_hash_span(serialized.span()); + return hash == *self.hash; + } +} + +#[cfg(test)] +mod tests { + // Core imports + + use debug::PrintTrait; + use poseidon::poseidon_hash_span; + + // Local imports + + use super::{Commitment, CommitmentTrait}; + + #[test] + #[available_gas(30_000)] + fn test_security_commit_reveal() { + let mut commitment = CommitmentTrait::new(); + let value = array!['ohayo'].span(); + let hash = poseidon_hash_span(value); + commitment.commit(hash); + let valid = commitment.reveal('ohayo'); + assert(valid, 'invalid reveal for commitment') + } + + #[test] + #[available_gas(15_000)] + #[should_panic(expected: ('Commitment: can not commit zero',))] + fn test_security_commit_revert_zero() { + let mut commitment = CommitmentTrait::new(); + let value = array!['ohayo'].span(); + let hash = poseidon_hash_span(value); + commitment.commit(0); + } +} diff --git a/crates/security/src/lib.cairo b/crates/security/src/lib.cairo new file mode 100644 index 00000000..48ed7d4b --- /dev/null +++ b/crates/security/src/lib.cairo @@ -0,0 +1 @@ +mod commitment;