├── .gas-snapshot ├── .gitattributes ├── .github └── workflows │ └── ci.yaml ├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── foundry.toml ├── src ├── InverseTrigonometry.sol ├── Trigonometry.sol └── generate_trigonometry.py └── test ├── InverseTrigonometry.t.sol ├── Trigonometry.t.sol └── trig.py /.gas-snapshot: -------------------------------------------------------------------------------- 1 | testCos1() (gas: 1880) 2 | testCos10() (gas: 2030) 3 | testCos11() (gas: 2078) 4 | testCos12() (gas: 2115) 5 | testCos13() (gas: 2073) 6 | testCos14() (gas: 2094) 7 | testCos15() (gas: 2170) 8 | testCos16() (gas: 2112) 9 | testCos17() (gas: 2300) 10 | testCos18() (gas: 2249) 11 | testCos19() (gas: 2306) 12 | testCos2() (gas: 2019) 13 | testCos20() (gas: 2193) 14 | testCos3() (gas: 1909) 15 | testCos4() (gas: 1955) 16 | testCos5() (gas: 1986) 17 | testCos6() (gas: 1966) 18 | testCos7() (gas: 2094) 19 | testCos8() (gas: 1999) 20 | testCos9() (gas: 2168) 21 | testNoReverts(uint256) (runs: 100000, μ: 1660, ~: 1664) 22 | testNoReverts(uint256) (runs: 100000, μ: 1669, ~: 1659) 23 | testSin1() (gas: 1928) 24 | testSin10() (gas: 1944) 25 | testSin11() (gas: 2060) 26 | testSin12() (gas: 2172) 27 | testSin13() (gas: 2155) 28 | testSin14() (gas: 2147) 29 | testSin15() (gas: 2177) 30 | testSin16() (gas: 1981) 31 | testSin17() (gas: 2201) 32 | testSin18() (gas: 2185) 33 | testSin19() (gas: 2309) 34 | testSin2() (gas: 1948) 35 | testSin20() (gas: 2085) 36 | testSin3() (gas: 2060) 37 | testSin4() (gas: 1984) 38 | testSin5() (gas: 2080) 39 | testSin6() (gas: 1812) 40 | testSin7() (gas: 1969) 41 | testSin8() (gas: 2045) 42 | testSin9() (gas: 2102) 43 | 44 | testArcsin1() (gas: 3804) 45 | testArcsin2() (gas: 9661) 46 | testArcsin3() (gas: 10135) 47 | testArcsin4() (gas: 9687) 48 | testArcsin5() (gas: 10161) 49 | testArcsin6() (gas: 9664) 50 | testArcsin7() (gas: 10138) 51 | testArcsin8() (gas: 9030) 52 | testArcsin9() (gas: 9503) 53 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity 2 | .gas-snapshot linguist-language=Julia -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | 8 | jobs: 9 | tests: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | with: 14 | submodules: recursive 15 | 16 | - name: Install Foundry 17 | uses: onbjerg/foundry-toolchain@v1 18 | with: 19 | version: nightly 20 | 21 | - name: Run tests 22 | run: FOUNDRY_PROFILE=ci forge test 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | cache/ 3 | out/ 4 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/prb-math"] 2 | path = lib/prb-math 3 | url = https://github.com/paulrberg/prb-math 4 | [submodule "lib/forge-std"] 5 | path = lib/forge-std 6 | url = https://github.com/foundry-rs/forge-std 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Matt Solomon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Solidity Trigonometry 2 | 3 | Solidity library offering basic trigonometry functions where inputs and outputs are integers. 4 | Inputs are specified in radians scaled by 1e18, and similarly outputs are scaled by 1e18. 5 | Each invocation of the `sin()` and `cos()` functions cost around 1600–1700 gas (see the `testNoReverts` costs in `.gas-snapshot` for more info). 6 | 7 | This implementation is based off the Solidity trigonometry library written by 8 | [Lefteris Karapetsas](https://twitter.com/LefterisJP) 9 | which can be found [here](https://github.com/Sikorkaio/sikorka/blob/e75c91925c914beaedf4841c0336a806f2b5f66d/contracts/trigonometry.sol). 10 | Compared to Lefteris' implementation, this version makes the following changes: 11 | - Uses a 32 bits instead of 16 bits for improved accuracy 12 | - Updated for Solidity 0.8.x 13 | - Various gas optimizations 14 | - Change inputs/outputs to standard trig format (scaled by 1e18) instead of requiring the integer format used by the algorithm 15 | 16 | The original implementation by Lefteris is based off Dave Dribin's [trigint](http://www.dribin.org/dave/trigint/) C library, 17 | which in turn is based on an [article](http://web.archive.org/web/20120301144605/http://www.dattalo.com/technical/software/pic/picsine.html) by Scott Dattalo. 18 | 19 | ## Usage 20 | 21 | When using this library, it's recommended to wrap input values (which are in radians) between `2 * PI * 1e18` and `4 * PI * 1e18` to avoid precision errors. 22 | This is equivalent to wrapping standard values between 0 and 2π. There is some flexibility on that range, but it should stay within reasonable bounds. 23 | 24 | To use this in a [Foundry](https://github.com/gakonst/foundry/) project, install it with: 25 | 26 | ```sh 27 | forge install https://github.com/mds1/solidity-trigonometry 28 | ``` 29 | 30 | To use this in a [dapptools](https://github.com/dapphub/dapptools/) project, install it with: 31 | 32 | ```sh 33 | dapp install https://github.com/mds1/solidity-trigonometry 34 | ``` 35 | 36 | There is currently no npm package, so for projects using npm for package management, such as [Hardhat](https://hardhat.org/) projects, use: 37 | 38 | ```sh 39 | yarn add https://github.com/mds1/solidity-trigonometry.git 40 | ``` 41 | 42 | ## Development 43 | 44 | ### Setup 45 | 46 | This library is developed with [Foundry](https://github.com/dapphub/dapptools/). 47 | If you don't have Foundry installed, run the command below to get `foundryup`, the Foundry toolchain installer: 48 | 49 | ``` 50 | curl -L https://foundry.paradigm.xyz | bash 51 | ``` 52 | 53 | Then in a new terminal session or after reloading your PATH, run `foundryup` to get the latest `forge` and `cast` binaries. 54 | 55 | 56 | ### Testing 57 | 58 | Run tests with `forge test`, and update gas snapshots with `FOUNDRY_FUZZ_RUNS=50000 forge snapshot` (this will take a while to run since that many FFI runs can be slow). 59 | 60 | NOTE: Tests are configured to run with the `--ffi` flag enabled for fuzz testing, so review the test commands before executing them to ensure you aren't running any malicious code on your machine. 61 | -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | src = 'src' 3 | out = 'out' 4 | libs = ['lib'] 5 | 6 | # test config 7 | ffi = true 8 | fuzz-runs = 100 9 | gas_reports = ["*"] 10 | verbosity = 3 11 | 12 | # solidity config 13 | solc-version = "0.8.13" 14 | optimizer = true 15 | optimizer-runs = 9999999 16 | 17 | [profile.ci] 18 | fuzz-runs = 50000 19 | -------------------------------------------------------------------------------- /src/InverseTrigonometry.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import {PRBMathSD59x18 as P} from "prb-math/PRBMathSD59x18.sol"; 5 | 6 | /** 7 | * @title Arcsine calculator. 8 | * @author Md Abid Sikder 9 | * 10 | * @notice Calculates arcsine. Fuzz testing shows that relative error is always 11 | * smaller than 0.01%. Uses the polynomial approximation functions found in 12 | * https://dsp.stackexchange.com/a/25771, but chooses between them at x=0.4788 13 | * due to differences in the relative errors as can be seen here 14 | * https://www.desmos.com/calculator/wrfwjhythe 15 | * 16 | * @dev See the desmos link for what functions f and g in the code refer to. 17 | */ 18 | library InverseTrigonometry { 19 | using P for int256; 20 | 21 | function g(int256 _x) internal pure returns (int256) { 22 | int256 ONE = 1000000000000000000; 23 | int256 TWO = 2000000000000000000; 24 | // 1.5707288 25 | int256 a0 = 1570728800000000000; 26 | // −0.2121144 27 | int256 a1 = -212114400000000000; 28 | // 0.0742610 29 | int256 a2 = 74261000000000000; 30 | // −0.0187293 31 | int256 a3 = -18729300000000000; 32 | 33 | int256 HALF_PI = P.pi().div(TWO); 34 | 35 | int256 root = P.sqrt(ONE - _x); 36 | 37 | return HALF_PI - root.mul(a0 + _x.mul(a1 + _x.mul(a2 + _x.mul(a3)))); 38 | } 39 | 40 | function f(int256 _x) internal pure returns (int256) { 41 | int256 ONE = 1000000000000000000; 42 | int256 xSq = _x.mul(_x); 43 | 44 | // 1/6 45 | // https://www.wolframalpha.com/input?i=1%2F6 46 | // 0.1666666666666666666666666 47 | int256 frac1Div6 = 166666666666666666; 48 | 49 | // 3/40 50 | // https://www.wolframalpha.com/input?i=3%2F40 51 | // 0.075 52 | int256 frac3Div40 = 75000000000000000; 53 | 54 | // 15/336 55 | // https://www.wolframalpha.com/input?i=15%2F336 56 | // 0.044642857142857142857142857142 57 | int256 frac15Div336= 44642857142857142; 58 | 59 | return _x.mul(ONE + xSq.mul(frac1Div6 + xSq.mul(frac3Div40 + xSq.mul(frac15Div336)))); 60 | } 61 | 62 | /** 63 | * @notice Arcsine function 64 | * 65 | * @param _x A integer with 18 fixed decimal points, where the whole part is bounded inside of [-1,1] 66 | * 67 | * @return The arcsine, with 18 fixed decimal points 68 | */ 69 | function arcsin(int256 _x) internal pure returns (int256) { 70 | int256 DOMAIN_MAX = 1000000000000000000; 71 | int256 DOMAIN_MIN = -DOMAIN_MAX; 72 | require(_x >= DOMAIN_MIN && _x <= DOMAIN_MAX); 73 | 74 | // arcsin is an odd function, so arcsin(-x) = -arcsin(x), so we can remove 75 | // the negative here for easier math 76 | bool isNegative = _x < 0; 77 | _x = isNegative ? -_x : _x; 78 | 79 | // 0.4788 80 | int256 CHOICE_LINE = 478800000000000000; 81 | 82 | int256 result = _x < CHOICE_LINE ? f(_x) : g(_x); 83 | 84 | return isNegative ? -result : result; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Trigonometry.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | /** 5 | * @notice Solidity library offering basic trigonometry functions where inputs and outputs are 6 | * integers. Inputs are specified in radians scaled by 1e18, and similarly outputs are scaled by 1e18. 7 | * 8 | * This implementation is based off the Solidity trigonometry library written by Lefteris Karapetsas 9 | * which can be found here: https://github.com/Sikorkaio/sikorka/blob/e75c91925c914beaedf4841c0336a806f2b5f66d/contracts/trigonometry.sol 10 | * 11 | * Compared to Lefteris' implementation, this version makes the following changes: 12 | * - Uses a 32 bits instead of 16 bits for improved accuracy 13 | * - Updated for Solidity 0.8.x 14 | * - Various gas optimizations 15 | * - Change inputs/outputs to standard trig format (scaled by 1e18) instead of requiring the 16 | * integer format used by the algorithm 17 | * 18 | * Lefertis' implementation is based off Dave Dribin's trigint C library 19 | * http://www.dribin.org/dave/trigint/ 20 | * 21 | * Which in turn is based from a now deleted article which can be found in the Wayback Machine: 22 | * http://web.archive.org/web/20120301144605/http://www.dattalo.com/technical/software/pic/picsine.html 23 | */ 24 | library Trigonometry { 25 | // Table index into the trigonometric table 26 | uint256 constant INDEX_WIDTH = 8; 27 | // Interpolation between successive entries in the table 28 | uint256 constant INTERP_WIDTH = 16; 29 | uint256 constant INDEX_OFFSET = 28 - INDEX_WIDTH; 30 | uint256 constant INTERP_OFFSET = INDEX_OFFSET - INTERP_WIDTH; 31 | uint32 constant ANGLES_IN_CYCLE = 1073741824; 32 | uint32 constant QUADRANT_HIGH_MASK = 536870912; 33 | uint32 constant QUADRANT_LOW_MASK = 268435456; 34 | uint256 constant SINE_TABLE_SIZE = 256; 35 | 36 | // Pi as an 18 decimal value, which is plenty of accuracy: "For JPL's highest accuracy calculations, which are for 37 | // interplanetary navigation, we use 3.141592653589793: https://www.jpl.nasa.gov/edu/news/2016/3/16/how-many-decimals-of-pi-do-we-really-need/ 38 | uint256 constant PI = 3141592653589793238; 39 | uint256 constant TWO_PI = 2 * PI; 40 | uint256 constant PI_OVER_TWO = PI / 2; 41 | 42 | // The constant sine lookup table was generated by generate_trigonometry.py. We must use a constant 43 | // bytes array because constant arrays are not supported in Solidity. Each entry in the lookup 44 | // table is 4 bytes. Since we're using 32-bit parameters for the lookup table, we get a table size 45 | // of 2^(32/4) + 1 = 257, where the first and last entries are equivalent (hence the table size of 46 | // 256 defined above) 47 | uint8 constant entry_bytes = 4; // each entry in the lookup table is 4 bytes 48 | uint256 constant entry_mask = ((1 << 8*entry_bytes) - 1); // mask used to cast bytes32 -> lookup table entry 49 | bytes constant sin_table = hex"00_00_00_00_00_c9_0f_88_01_92_1d_20_02_5b_26_d7_03_24_2a_bf_03_ed_26_e6_04_b6_19_5d_05_7f_00_35_06_47_d9_7c_07_10_a3_45_07_d9_5b_9e_08_a2_00_9a_09_6a_90_49_0a_33_08_bc_0a_fb_68_05_0b_c3_ac_35_0c_8b_d3_5e_0d_53_db_92_0e_1b_c2_e4_0e_e3_87_66_0f_ab_27_2b_10_72_a0_48_11_39_f0_cf_12_01_16_d5_12_c8_10_6e_13_8e_db_b1_14_55_76_b1_15_1b_df_85_15_e2_14_44_16_a8_13_05_17_6d_d9_de_18_33_66_e8_18_f8_b8_3c_19_bd_cb_f3_1a_82_a0_25_1b_47_32_ef_1c_0b_82_6a_1c_cf_8c_b3_1d_93_4f_e5_1e_56_ca_1e_1f_19_f9_7b_1f_dc_dc_1b_20_9f_70_1c_21_61_b3_9f_22_23_a4_c5_22_e5_41_af_23_a6_88_7e_24_67_77_57_25_28_0c_5d_25_e8_45_b6_26_a8_21_85_27_67_9d_f4_28_26_b9_28_28_e5_71_4a_29_a3_c4_85_2a_61_b1_01_2b_1f_34_eb_2b_dc_4e_6f_2c_98_fb_ba_2d_55_3a_fb_2e_11_0a_62_2e_cc_68_1e_2f_87_52_62_30_41_c7_60_30_fb_c5_4d_31_b5_4a_5d_32_6e_54_c7_33_26_e2_c2_33_de_f2_87_34_96_82_4f_35_4d_90_56_36_04_1a_d9_36_ba_20_13_37_6f_9e_46_38_24_93_b0_38_d8_fe_93_39_8c_dd_32_3a_40_2d_d1_3a_f2_ee_b7_3b_a5_1e_29_3c_56_ba_70_3d_07_c1_d5_3d_b8_32_a5_3e_68_0b_2c_3f_17_49_b7_3f_c5_ec_97_40_73_f2_1d_41_21_58_9a_41_ce_1e_64_42_7a_41_d0_43_25_c1_35_43_d0_9a_ec_44_7a_cd_50_45_24_56_bc_45_cd_35_8f_46_75_68_27_47_1c_ec_e6_47_c3_c2_2e_48_69_e6_64_49_0f_57_ee_49_b4_15_33_4a_58_1c_9d_4a_fb_6c_97_4b_9e_03_8f_4c_3f_df_f3_4c_e1_00_34_4d_81_62_c3_4e_21_06_17_4e_bf_e8_a4_4f_5e_08_e2_4f_fb_65_4c_50_97_fc_5e_51_33_cc_94_51_ce_d4_6e_52_69_12_6e_53_02_85_17_53_9b_2a_ef_54_33_02_7d_54_ca_0a_4a_55_60_40_e2_55_f5_a4_d2_56_8a_34_a9_57_1d_ee_f9_57_b0_d2_55_58_42_dd_54_58_d4_0e_8c_59_64_64_97_59_f3_de_12_5a_82_79_99_5b_10_35_ce_5b_9d_11_53_5c_29_0a_cc_5c_b4_20_df_5d_3e_52_36_5d_c7_9d_7b_5e_50_01_5d_5e_d7_7c_89_5f_5e_0d_b2_5f_e3_b3_8d_60_68_6c_ce_60_ec_38_2f_61_6f_14_6b_61_f1_00_3e_62_71_fa_68_62_f2_01_ac_63_71_14_cc_63_ef_32_8f_64_6c_59_bf_64_e8_89_25_65_63_bf_91_65_dd_fb_d2_66_57_3c_bb_66_cf_81_1f_67_46_c7_d7_67_bd_0f_bc_68_32_57_aa_68_a6_9e_80_69_19_e3_1f_69_8c_24_6b_69_fd_61_4a_6a_6d_98_a3_6a_dc_c9_64_6b_4a_f2_78_6b_b8_12_d0_6c_24_29_5f_6c_8f_35_1b_6c_f9_34_fb_6d_62_27_f9_6d_ca_0d_14_6e_30_e3_49_6e_96_a9_9c_6e_fb_5f_11_6f_5f_02_b1_6f_c1_93_84_70_23_10_99_70_83_78_fe_70_e2_cb_c5_71_41_08_04_71_9e_2c_d1_71_fa_39_48_72_55_2c_84_72_af_05_a6_73_07_c3_cf_73_5f_66_25_73_b5_eb_d0_74_0b_53_fa_74_5f_9d_d0_74_b2_c8_83_75_04_d3_44_75_55_bd_4b_75_a5_85_ce_75_f4_2c_0a_76_41_af_3c_76_8e_0e_a5_76_d9_49_88_77_23_5f_2c_77_6c_4e_da_77_b4_17_df_77_fa_b9_88_78_40_33_28_78_84_84_13_78_c7_ab_a1_79_09_a9_2c_79_4a_7c_11_79_8a_23_b0_79_c8_9f_6d_7a_05_ee_ac_7a_42_10_d8_7a_7d_05_5a_7a_b6_cb_a3_7a_ef_63_23_7b_26_cb_4e_7b_5d_03_9d_7b_92_0b_88_7b_c5_e2_8f_7b_f8_88_2f_7c_29_fb_ed_7c_5a_3d_4f_7c_89_4b_dd_7c_b7_27_23_7c_e3_ce_b1_7d_0f_42_17_7d_39_80_eb_7d_62_8a_c5_7d_8a_5f_3f_7d_b0_fd_f7_7d_d6_66_8e_7d_fa_98_a7_7e_1d_93_e9_7e_3f_57_fe_7e_5f_e4_92_7e_7f_39_56_7e_9d_55_fb_7e_ba_3a_38_7e_d5_e5_c5_7e_f0_58_5f_7f_09_91_c3_7f_21_91_b3_7f_38_57_f5_7f_4d_e4_50_7f_62_36_8e_7f_75_4e_7f_7f_87_2b_f2_7f_97_ce_bc_7f_a7_36_b3_7f_b5_63_b2_7f_c2_55_95_7f_ce_0c_3d_7f_d8_87_8d_7f_e1_c7_6a_7f_e9_cb_bf_7f_f0_94_77_7f_f6_21_81_7f_fa_72_d0_7f_fd_88_59_7f_ff_62_15_7f_ff_ff_ff"; 50 | 51 | /** 52 | * @notice Return the sine of a value, specified in radians scaled by 1e18 53 | * @dev This algorithm for converting sine only uses integer values, and it works by dividing the 54 | * circle into 30 bit angles, i.e. there are 1,073,741,824 (2^30) angle units, instead of the 55 | * standard 360 degrees (2pi radians). From there, we get an output in range -2,147,483,647 to 56 | * 2,147,483,647, (which is the max value of an int32) which is then converted back to the standard 57 | * range of -1 to 1, again scaled by 1e18 58 | * @param _angle Angle to convert 59 | * @return Result scaled by 1e18 60 | */ 61 | function sin(uint256 _angle) internal pure returns (int256) { 62 | unchecked { 63 | // Convert angle from from arbitrary radian value (range of 0 to 2pi) to the algorithm's range 64 | // of 0 to 1,073,741,824 65 | _angle = ANGLES_IN_CYCLE * (_angle % TWO_PI) / TWO_PI; 66 | 67 | // Apply a mask on an integer to extract a certain number of bits, where angle is the integer 68 | // whose bits we want to get, the width is the width of the bits (in bits) we want to extract, 69 | // and the offset is the offset of the bits (in bits) we want to extract. The result is an 70 | // integer containing _width bits of _value starting at the offset bit 71 | uint256 interp = (_angle >> INTERP_OFFSET) & ((1 << INTERP_WIDTH) - 1); 72 | uint256 index = (_angle >> INDEX_OFFSET) & ((1 << INDEX_WIDTH) - 1); 73 | 74 | // The lookup table only contains data for one quadrant (since sin is symmetric around both 75 | // axes), so here we figure out which quadrant we're in, then we lookup the values in the 76 | // table then modify values accordingly 77 | bool is_odd_quadrant = (_angle & QUADRANT_LOW_MASK) == 0; 78 | bool is_negative_quadrant = (_angle & QUADRANT_HIGH_MASK) != 0; 79 | 80 | if (!is_odd_quadrant) { 81 | index = SINE_TABLE_SIZE - 1 - index; 82 | } 83 | 84 | bytes memory table = sin_table; 85 | // We are looking for two consecutive indices in our lookup table 86 | // Since EVM is left aligned, to read n bytes of data from idx i, we must read from `i * data_len` + `n` 87 | // therefore, to read two entries of size entry_bytes `index * entry_bytes` + `entry_bytes * 2` 88 | uint256 offset1_2 = (index + 2) * entry_bytes; 89 | 90 | // This following snippet will function for any entry_bytes <= 15 91 | uint256 x1_2; assembly { 92 | // mload will grab one word worth of bytes (32), as that is the minimum size in EVM 93 | x1_2 := mload(add(table, offset1_2)) 94 | } 95 | 96 | // We now read the last two numbers of size entry_bytes from x1_2 97 | // in example: entry_bytes = 4; x1_2 = 0x00...12345678abcdefgh 98 | // therefore: entry_mask = 0xFFFFFFFF 99 | 100 | // 0x00...12345678abcdefgh >> 8*4 = 0x00...12345678 101 | // 0x00...12345678 & 0xFFFFFFFF = 0x12345678 102 | uint256 x1 = x1_2 >> 8*entry_bytes & entry_mask; 103 | // 0x00...12345678abcdefgh & 0xFFFFFFFF = 0xabcdefgh 104 | uint256 x2 = x1_2 & entry_mask; 105 | 106 | // Approximate angle by interpolating in the table, accounting for the quadrant 107 | uint256 approximation = ((x2 - x1) * interp) >> INTERP_WIDTH; 108 | int256 sine = is_odd_quadrant ? int256(x1) + int256(approximation) : int256(x2) - int256(approximation); 109 | if (is_negative_quadrant) { 110 | sine *= -1; 111 | } 112 | 113 | // Bring result from the range of -2,147,483,647 through 2,147,483,647 to -1e18 through 1e18. 114 | // This can never overflow because sine is bounded by the above values 115 | return sine * 1e18 / 2_147_483_647; 116 | } 117 | } 118 | 119 | /** 120 | * @notice Return the cosine of a value, specified in radians scaled by 1e18 121 | * @dev This is identical to the sin() method, and just computes the value by delegating to the 122 | * sin() method using the identity cos(x) = sin(x + pi/2) 123 | * @dev Overflow when `angle + PI_OVER_TWO > type(uint256).max` is ok, results are still accurate 124 | * @param _angle Angle to convert 125 | * @return Result scaled by 1e18 126 | */ 127 | function cos(uint256 _angle) internal pure returns (int256) { 128 | unchecked { 129 | return sin(_angle + PI_OVER_TWO); 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/generate_trigonometry.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | Originally written by Lefteris Karapetsas (https://twitter.com/LefterisJP), with the source here: 5 | https://github.com/Sikorkaio/sikorka/blob/e75c91925c914beaedf4841c0336a806f2b5f66d/scripts/generate_trigonometry.py 6 | 7 | This version is nearly identical to the original, and is kept in the repo for posterity in case 8 | the original version is ever removed. This script was run with `number_of_bits = 32` to 9 | generate the values used in the library. 10 | 11 | Note that the regexes used in this script are tailored to the original version of Trigonometry.sol 12 | by Lefteris, and therefore this script will not succeed if ran against the version in this repo. 13 | This is because the library was reformatted to reduce gas usage. If you need to regenerate the 14 | values, either update the regexes or run this script against the original Trigonometry.sol contract: 15 | https://github.com/Sikorkaio/sikorka/blob/e75c91925c914beaedf4841c0336a806f2b5f66d/contracts/trigonometry.sol 16 | """ 17 | 18 | from __future__ import division 19 | import os 20 | import math 21 | import re 22 | import sys 23 | 24 | 25 | def re_replace_constant(string, typename, varname, value): 26 | constant_re = re.compile( 27 | r"({} +constant +{} +=) +(.*);".format(typename, varname) 28 | ) 29 | match = constant_re.search(string) 30 | if not match: 31 | print( 32 | "ERROR: Could not match RE for '{}' during template generation.". 33 | format(varname) 34 | ) 35 | sys.exit(1) 36 | 37 | if match.groups()[1] == str(value): 38 | # The value already exists in the source 39 | return string 40 | 41 | new_string = constant_re.sub(r"\1 {};".format(str(value)), string) 42 | return new_string 43 | 44 | 45 | def re_replace_constant_and_type(string, typename, varname, value): 46 | constant_re = re.compile( 47 | r"( *)(.*) +constant +({}) += +(.*);".format(varname) 48 | ) 49 | match = constant_re.search(string) 50 | if not match: 51 | print( 52 | "ERROR: Could not match RE for '{}' during template generation.". 53 | format(varname) 54 | ) 55 | sys.exit(1) 56 | 57 | if match.groups()[1] == typename and match.groups()[3] == str(value): 58 | # The variable already exists in the source as we want it 59 | return string 60 | 61 | new_string = constant_re.sub( 62 | r"\1{} constant \3 = {};".format(typename, str(value)), 63 | string 64 | ) 65 | return new_string 66 | 67 | 68 | def re_replace_vardecl(string, typename, varname): 69 | var_re = re.compile( 70 | r"( *)(uint.*) +({});".format(varname) 71 | ) 72 | match = var_re.search(string) 73 | if not match: 74 | print( 75 | "ERROR: Could not match RE for '{}' during template generation.". 76 | format(varname) 77 | ) 78 | sys.exit(1) 79 | 80 | if match.groups()[1] == typename: 81 | # The variable already exists in the source as we want it 82 | return string 83 | 84 | new_string = var_re.sub( 85 | r"\1{} {};".format(typename, varname), 86 | string 87 | ) 88 | return new_string 89 | 90 | 91 | def re_replace_function_params(string, func_name, param_type): 92 | func_re = re.compile( 93 | r"( *)function {}\((.*?) *_angle\)".format(func_name) 94 | ) 95 | match = func_re.search(string) 96 | if not match: 97 | print( 98 | "ERROR: Could not match function '{}' during template generation.". 99 | format(func_name) 100 | ) 101 | sys.exit(1) 102 | 103 | if match.groups()[1] == param_type: 104 | # The type already exists in the source as we want it 105 | return string 106 | 107 | new_string = func_re.sub( 108 | r"\1function {}({} _angle)".format(func_name, param_type), 109 | string 110 | ) 111 | return new_string 112 | 113 | 114 | def re_replace_function_return(string, func_name, param_type): 115 | func_re = re.compile( 116 | r"( *)function {}\((.*)\)(.*)returns *\((.*?)\)".format(func_name) 117 | ) 118 | match = func_re.search(string) 119 | if not match: 120 | print( 121 | "ERROR: Could not match function '{}' during template generation.". 122 | format(func_name) 123 | ) 124 | sys.exit(1) 125 | 126 | if match.groups()[3] == param_type: 127 | # The type already exists in the source as we want it 128 | return string 129 | 130 | new_string = func_re.sub( 131 | r"\1function {}(\2)\3returns ({})".format(func_name, param_type), 132 | string 133 | ) 134 | return new_string 135 | 136 | 137 | def re_replace_comments(string, number_of_bits): 138 | angles_in_circle = 1 << (number_of_bits - 2) 139 | amplitude = (1 << (number_of_bits - 1)) - 1 140 | comment_re = re.compile( 141 | r'( *)\* @param _angle A (\d+)-bit angle. This divides' 142 | ' the circle into (\d+)' 143 | ) 144 | match = comment_re.search(string) 145 | if not match: 146 | print( 147 | "ERROR: Could not match angle comment during template generation." 148 | ) 149 | sys.exit(1) 150 | if ( 151 | int(match.groups()[1]) != number_of_bits - 2 152 | or int(match.groups()[2]) == angles_in_circle 153 | ): 154 | new_string = comment_re.sub( 155 | r'\1* @param _angle A {}-bit angle. This divides' 156 | 'the circle into {}'.format( 157 | str(number_of_bits - 2), 158 | angles_in_circle 159 | ), 160 | string 161 | ) 162 | 163 | string = new_string 164 | comment_re = re.compile( 165 | r'( *)\* @return The sine result as a number in the range ' 166 | '-(\d+) to (\d+)' 167 | ) 168 | if not match: 169 | print( 170 | "ERROR: Could not match return comment during template generation." 171 | ) 172 | sys.exit(1) 173 | if ( 174 | int(match.groups()[1]) == amplitude 175 | and int(match.groups()[2]) == amplitude 176 | ): 177 | # The comment already exists in the source as we want it 178 | return string 179 | 180 | new_string = comment_re.sub( 181 | r'\1* @return The sine result as a number ' 182 | 'in the range -{} to {}'.format( 183 | amplitude, 184 | amplitude, 185 | ), 186 | string 187 | ) 188 | 189 | return new_string 190 | 191 | 192 | def gen_sin_table(number_of_bits, table_size): 193 | table = '"' 194 | number_of_bytes = int(number_of_bits / 8) 195 | amplitude = (1 << (number_of_bits - 1)) - 1 196 | for i in range(0, table_size): 197 | radians = (i * (math.pi / 2)) / (table_size - 1) 198 | sin_value = amplitude * math.sin(radians) 199 | table_value = round(sin_value) 200 | hex_value = "{0:0{1}x}".format(int(table_value), 2 * number_of_bytes) 201 | table += '\\x' + '\\x'.join( 202 | hex_value[i: i + 2] for i in range(0, len(hex_value), 2) 203 | ) 204 | return table + '"' 205 | 206 | 207 | def generate_trigonometry(number_of_bits, for_tests): 208 | print("Generating the sin() lookup table ...") 209 | table_size = (2 ** int(number_of_bits / 4)) + 1 210 | if number_of_bits % 8 != 0: 211 | print("ERROR: Bits should be a multiple of 8") 212 | sys.exit(1) 213 | 214 | path = os.path.join( 215 | os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 216 | 'contracts', 217 | 'trigonometry.sol' 218 | ) 219 | with open(path) as f: 220 | lines = f.read() 221 | 222 | uint_type_name = 'uint' + str(number_of_bits) 223 | lines = re_replace_constant( 224 | lines, 225 | 'uint8', 226 | 'entry_bytes', 227 | int(number_of_bits / 8) 228 | ) 229 | lines = re_replace_constant( 230 | lines, 231 | 'uint', 232 | 'INDEX_WIDTH', 233 | int(number_of_bits / 4) 234 | ) 235 | lines = re_replace_constant( 236 | lines, 237 | 'uint', 238 | 'INTERP_WIDTH', 239 | int(number_of_bits / 2) 240 | ) 241 | lines = re_replace_constant( 242 | lines, 243 | 'uint', 244 | 'INDEX_OFFSET', 245 | '{} - INDEX_WIDTH'.format(number_of_bits - 4) 246 | ) 247 | lines = re_replace_vardecl(lines, uint_type_name, 'trigint_value') 248 | lines = re_replace_constant_and_type( 249 | lines, 250 | uint_type_name, 251 | 'ANGLES_IN_CYCLE', 252 | 1 << (number_of_bits - 2) 253 | ) 254 | lines = re_replace_constant( 255 | lines, 256 | 'uint', 257 | 'SINE_TABLE_SIZE', 258 | table_size - 1 259 | ) 260 | lines = re_replace_constant_and_type( 261 | lines, 262 | uint_type_name, 263 | 'QUADRANT_HIGH_MASK', 264 | int(1 << (number_of_bits - 3)) 265 | ) 266 | lines = re_replace_constant_and_type( 267 | lines, 268 | uint_type_name, 269 | 'QUADRANT_LOW_MASK', 270 | int(1 << (number_of_bits - 4)) 271 | ) 272 | lines = re_replace_constant( 273 | lines, 274 | 'bytes', 275 | 'sin_table', 276 | gen_sin_table(number_of_bits, table_size) 277 | ) 278 | lines = re_replace_function_params( 279 | lines, 280 | 'sin', 281 | uint_type_name 282 | ) 283 | lines = re_replace_function_params( 284 | lines, 285 | 'cos', 286 | uint_type_name 287 | ) 288 | lines = re_replace_function_return( 289 | lines, 290 | 'sin_table_lookup', 291 | uint_type_name 292 | ) 293 | lines = re_replace_comments(lines, number_of_bits) 294 | 295 | if for_tests: 296 | path = os.path.join( 297 | os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 298 | 'contracts', 299 | 'trigonometry_generated.sol' 300 | ) 301 | lines = lines.replace( 302 | 'library Trigonometry', 303 | 'library TrigonometryGenerated' 304 | ) 305 | 306 | with open(path, 'w') as f: 307 | f.write(lines) 308 | 309 | 310 | if __name__ == '__main__': 311 | number_of_bits = 32 312 | generate_trigonometry(number_of_bits, for_tests=False) -------------------------------------------------------------------------------- /test/InverseTrigonometry.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import "forge-std/Test.sol"; 5 | import {InverseTrigonometry as A} from "src/InverseTrigonometry.sol"; 6 | import {PRBMathSD59x18 as P} from "prb-math/PRBMathSD59x18.sol"; 7 | 8 | contract ArcsinTest is Test { 9 | using P for int256; 10 | 11 | // 1e14 = 0.01% relative error tolerance 12 | uint256 constant TOL = 1e14; 13 | 14 | /* 15 | * The pairs of points we are testing. Their negative versions are also 16 | * tested. 17 | * 18 | * sin(0) = 0 --> 0 = arcsin(0) 19 | * sin(π/6) = 1/2 --> π/6 = arcsin(1/2) 20 | * sin(π/4) = 1/√2 --> π/4 = arcsin(1/√2) 21 | * sin(π/3) = √3/2 --> π/3 = arcsin(√3/2) 22 | * sin(π/2) = 1 --> π/2 = arcsin(1) 23 | */ 24 | 25 | int256 PI_OVER_TWO = P.pi().div(P.fromInt(2)); 26 | int256 PI_OVER_THREE = P.pi().div(P.fromInt(3)); 27 | int256 PI_OVER_FOUR = P.pi().div(P.fromInt(4)); 28 | int256 PI_OVER_SIX = P.pi().div(P.fromInt(6)); 29 | 30 | int256 ONE = P.fromInt(1); 31 | int256 TWO = P.fromInt(2); 32 | int256 THREE = P.fromInt(3); 33 | int256 ONE_HALF = ONE.div(TWO); 34 | int256 ONE_OVER_ROOT_TWO = ONE.div(P.sqrt(TWO)); 35 | int256 ROOT_THREE_OVER_TWO = P.sqrt(THREE).div(TWO); 36 | 37 | // copied from DSTestPlus.sol 38 | // Convert uint to string, from https://github.com/mzhu25/sol2string/blob/13f566f7dc61c820c24a673da72d0114183a17c8/contracts/LibUintToString.sol 39 | uint256 private constant MAX_UINT256_STRING_LENGTH = 78; 40 | uint8 private constant ASCII_DIGIT_OFFSET = 48; 41 | function uintToString(uint256 n) internal pure returns (string memory nstr) { 42 | if (n == 0) return "0"; 43 | 44 | // Overallocate memory 45 | nstr = new string(MAX_UINT256_STRING_LENGTH); 46 | uint256 k = MAX_UINT256_STRING_LENGTH; 47 | // Populate string from right to left (lsb to msb). 48 | while (n != 0) { 49 | assembly { 50 | let char := add(ASCII_DIGIT_OFFSET, mod(n, 10)) 51 | mstore(add(nstr, k), char) 52 | k := sub(k, 1) 53 | n := div(n, 10) 54 | } 55 | } 56 | assembly { 57 | nstr := add(nstr, k) // shift pointer over to actual start of string 58 | mstore(nstr, sub(MAX_UINT256_STRING_LENGTH, k)) // store actual string length 59 | } 60 | return nstr; 61 | } 62 | 63 | function testArcsinFuzz(uint256 _x) public { 64 | uint256 DOMAIN_MAX = 1000000000000000000; 65 | _x = bound(_x, 0, DOMAIN_MAX); 66 | 67 | string[] memory inputs = new string[](4); 68 | inputs[0] = "python3"; 69 | inputs[1] = "test/trig.py"; 70 | inputs[2] = "arcsin"; 71 | inputs[3] = uintToString(_x); 72 | 73 | bytes memory ret = vm.ffi(inputs); 74 | (int256 output) = abi.decode(ret, (int256)); 75 | assertApproxEqRel(A.arcsin(int256(_x)), output, TOL); 76 | } 77 | 78 | function testNoReverts(int256 _x) public { 79 | int256 DOMAIN_MAX = 1000000000000000000; 80 | int256 DOMAIN_MIN = -DOMAIN_MAX; 81 | vm.assume(_x >= DOMAIN_MIN && _x <= DOMAIN_MAX); 82 | 83 | A.arcsin(_x); 84 | } 85 | 86 | function testArcsin1() public { 87 | int256 actual = A.arcsin(0); 88 | int256 expected = 0; 89 | assertApproxEqRel(actual, expected, TOL); 90 | } 91 | 92 | function testArcsin2() public { 93 | int256 actual = A.arcsin(ONE_HALF); 94 | int256 expected = PI_OVER_SIX; 95 | assertApproxEqRel(actual, expected, TOL); 96 | } 97 | 98 | function testArcsin3() public { 99 | int256 actual = A.arcsin(-ONE_HALF); 100 | int256 expected = -PI_OVER_SIX; 101 | assertApproxEqRel(actual, expected, TOL); 102 | } 103 | function testArcsin4() public { 104 | int256 actual = A.arcsin(ONE_OVER_ROOT_TWO); 105 | int256 expected = PI_OVER_FOUR; 106 | assertApproxEqRel(actual, expected, TOL); 107 | } 108 | function testArcsin5() public { 109 | int256 actual = A.arcsin(-ONE_OVER_ROOT_TWO); 110 | int256 expected = -PI_OVER_FOUR; 111 | assertApproxEqRel(actual, expected, TOL); 112 | } 113 | function testArcsin6() public { 114 | int256 actual = A.arcsin(ROOT_THREE_OVER_TWO); 115 | int256 expected = PI_OVER_THREE; 116 | assertApproxEqRel(actual, expected, TOL); 117 | } 118 | function testArcsin7() public { 119 | int256 actual = A.arcsin(-ROOT_THREE_OVER_TWO); 120 | int256 expected = -PI_OVER_THREE; 121 | assertApproxEqRel(actual, expected, TOL); 122 | } 123 | function testArcsin8() public { 124 | int256 actual = A.arcsin(ONE); 125 | int256 expected = PI_OVER_TWO; 126 | assertApproxEqRel(actual, expected, TOL); 127 | } 128 | function testArcsin9() public { 129 | int256 actual = A.arcsin(-ONE); 130 | int256 expected = -PI_OVER_TWO; 131 | assertApproxEqRel(actual, expected, TOL); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /test/Trigonometry.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "forge-std/Test.sol"; 5 | import "src/Trigonometry.sol"; 6 | 7 | /** 8 | * @dev A note about precision and fuzz testing this library. 9 | * 10 | * These fuzz test bound the generated angle to avoid precision errors. Using floating point inputs 11 | * that are too large, whether with python's math.sin, numpy's sin, or mpmath's sin all result in 12 | * precision errors at large values. Entering the large decimal values into WolframAlpha gives 13 | * accurate results, but using their API would be very slow for fuzz testing. 14 | * 15 | * For example, let's take π/8, and add 1e18 * 2π. The sin of that value should match sin(π/8) = 0.3827. 16 | * As an integer for solidity, this gives us (π * 1e18) / 8 + 1e18 * (2π * 1e18) = 6283185307179586477317985848257729923 17 | * or 6283185307179586477.317985848257729923 as a decimal. First we check Wolfram Alpha, which 18 | * as expected returns 0.3827 from this query: https://www.wolframalpha.com/input?i=sin%286283185307179586477.317985848257729923%29 19 | * 20 | * This solidity implementation returns the same value as verified in `testSin12()` If we try to 21 | * replicate this in python, we get the wrong reult: 22 | * 23 | * >>> import math 24 | * >>> import numpy as np 25 | * >>> from mpmath import mp 26 | * >>> math.sin(6283185307179586477.317985848257729923) 27 | * 0.9842895889634229 28 | * >>> np.sin(6283185307179586477.317985848257729923) 29 | * 0.9842895889634229 30 | * >>> mp.sin(6283185307179586477.317985848257729923) 31 | * mpf('0.98428958896342289') 32 | * 33 | * You can take the input modulo 2π first to reduce error, but that still results in output values 34 | * of about 0.9576, instead of the expected 0.3827. 35 | * 36 | * Similarly, for values too small, solidity's integer division can results in errors. As a result, 37 | * we simply bound the input to a minimum of 2πe18 with a reasonably large maximum, to ensure the 38 | * tests do not fail due to precision errors. 39 | * 40 | * Consequently, when using this library, you should also bound your inputs to a reasonable range. 41 | */ 42 | 43 | contract TestTrigonometry { 44 | function sin(uint256 _angle) public pure returns (int256) { 45 | return Trigonometry.sin(_angle); 46 | } 47 | 48 | function cos(uint256 _angle) public pure returns (int256) { 49 | return Trigonometry.cos(_angle); 50 | } 51 | } 52 | 53 | contract TrigonometryTest is Test { 54 | TestTrigonometry trig; 55 | uint256 constant SCALE = 1e18 * 2 * PI; // scale to add to trig inputs so same output is expected 56 | uint256 constant PI = 3141592653589793238; // π as an 18 decimal value (wad), must match the value in Trigonometry.sol 57 | uint256 constant TOL = 1.5e14; // relative tolerance, as a wad where 1e18 = 100%, so 1e14 = 0.015% 58 | 59 | function setUp() public { 60 | trig = new TestTrigonometry(); 61 | } 62 | } 63 | 64 | contract Sine is TrigonometryTest { 65 | // --- Fuzz --- 66 | function testNoReverts(uint256 _angle) public view { 67 | trig.sin(_angle); 68 | } 69 | 70 | function testSinFuzz(uint256 _angle) public { 71 | _angle = bound(_angle, PI, 2000 * PI); 72 | 73 | string[] memory inputs = new string[](4); 74 | inputs[0] = "python3"; 75 | inputs[1] = "test/trig.py"; 76 | inputs[2] = "sin"; 77 | inputs[3] = vm.toString(_angle); 78 | 79 | bytes memory ret = vm.ffi(inputs); 80 | (int256 output) = abi.decode(ret, (int256)); 81 | assertApproxEqRel(trig.sin(_angle), output, TOL); 82 | } 83 | 84 | // --- Angles between 0 <= x <= 2π --- 85 | function testSin1() public { 86 | assertApproxEqRel(trig.sin(0), 0, TOL); 87 | } 88 | function testSin2() public { 89 | assertApproxEqRel(trig.sin(PI / 8), 382683432365089800, TOL); 90 | } 91 | function testSin3() public { 92 | assertApproxEqRel(trig.sin(PI / 4), 707106781186547500, TOL); 93 | } 94 | function testSin4() public { 95 | assertApproxEqRel(trig.sin(PI / 2), 1e18, TOL); 96 | } 97 | function testSin5() public { 98 | assertApproxEqRel(trig.sin(PI * 3 / 4), 707106781186547600, TOL); 99 | } 100 | function testSin6() public { 101 | assertApproxEqRel(trig.sin(PI), 0, TOL); 102 | } 103 | function testSin7() public { 104 | assertApproxEqRel(trig.sin(PI * 5 / 4), -707106781186547600, TOL); 105 | } 106 | function testSin8() public { 107 | assertApproxEqRel(trig.sin(PI * 3 / 2), -1e18, TOL); 108 | } 109 | function testSin9() public { 110 | assertApproxEqRel(trig.sin(PI * 7 / 4), -707106781186547600, TOL); 111 | } 112 | function testSin10() public { 113 | assertApproxEqRel(trig.sin(PI * 2), 0, TOL); 114 | } 115 | 116 | // --- Angles above 2π that must be wrapped --- 117 | function testSin11() public { 118 | assertApproxEqRel(trig.sin(SCALE + 0), 0, TOL); 119 | } 120 | function testSin12() public { 121 | assertApproxEqRel(trig.sin(SCALE + PI / 8), 382683432365089800, TOL); 122 | } 123 | function testSin13() public { 124 | assertApproxEqRel(trig.sin(SCALE + PI / 4), 707106781186547500, TOL); 125 | } 126 | function testSin14() public { 127 | assertApproxEqRel(trig.sin(SCALE + PI / 2), 1e18, TOL); 128 | } 129 | function testSin15() public { 130 | assertApproxEqRel(trig.sin(SCALE + PI * 3 / 4), 707106781186547600, TOL); 131 | } 132 | function testSin16() public { 133 | assertApproxEqRel(trig.sin(SCALE + PI), 0, TOL); 134 | } 135 | function testSin17() public { 136 | assertApproxEqRel(trig.sin(SCALE + PI * 5 / 4), -707106781186547600, TOL); 137 | } 138 | function testSin18() public { 139 | assertApproxEqRel(trig.sin(SCALE + PI * 3 / 2), -1e18, TOL); 140 | } 141 | function testSin19() public { 142 | assertApproxEqRel(trig.sin(SCALE + PI * 7 / 4), -707106781186547600, TOL); 143 | } 144 | function testSin20() public { 145 | assertApproxEqRel(trig.sin(SCALE + PI * 2), 0, TOL); 146 | } 147 | } 148 | 149 | contract Cosine is TrigonometryTest { 150 | // --- Fuzz --- 151 | function testNoReverts(uint256 _angle) public view { 152 | trig.cos(_angle); 153 | } 154 | 155 | function testCosFuzz(uint256 _angle) public { 156 | _angle = bound(_angle, PI, 2000 * PI); 157 | 158 | string[] memory inputs = new string[](4); 159 | inputs[0] = "python3"; 160 | inputs[1] = "test/trig.py"; 161 | inputs[2] = "cos"; 162 | inputs[3] = vm.toString(_angle); 163 | 164 | bytes memory ret = vm.ffi(inputs); 165 | (int256 output) = abi.decode(ret, (int256)); 166 | assertApproxEqRel(trig.cos(_angle), output, TOL); 167 | } 168 | 169 | // --- Angles between 0 <= x <= 2π --- 170 | function testCos1() public { 171 | assertApproxEqRel(trig.cos(0), 1e18, TOL); 172 | } 173 | function testCos2() public { 174 | assertApproxEqRel(trig.cos(PI / 8), 923879532511286756, TOL); 175 | } 176 | function testCos3() public { 177 | assertApproxEqRel(trig.cos(PI / 4), 707106781186547600, TOL); 178 | } 179 | function testCos4() public { 180 | assertApproxEqRel(trig.cos(PI / 2), 0, TOL); 181 | } 182 | function testCos5() public { 183 | assertApproxEqRel(trig.cos(PI * 3 / 4), -707106781186547600, TOL); 184 | } 185 | function testCos6() public { 186 | assertApproxEqRel(trig.cos(PI), -1e18, TOL); 187 | } 188 | function testCos7() public { 189 | assertApproxEqRel(trig.cos(PI * 5 / 4), -707106781186547600, TOL); 190 | } 191 | function testCos8() public { 192 | assertApproxEqRel(trig.cos(PI * 3 / 2), 0, TOL); 193 | } 194 | function testCos9() public { 195 | assertApproxEqRel(trig.cos(PI * 7 / 4), 707106781186547600, TOL); 196 | } 197 | function testCos10() public { 198 | assertApproxEqRel(trig.cos(PI * 2), 1e18, TOL); 199 | } 200 | 201 | // --- Angles above 2π that must be wrapped --- 202 | function testCos11() public { 203 | assertApproxEqRel(trig.cos(SCALE + 0), 1e18, TOL); 204 | } 205 | function testCos12() public { 206 | assertApproxEqRel(trig.cos(SCALE + PI / 8), 923879532511286756, TOL); 207 | } 208 | function testCos13() public { 209 | assertApproxEqRel(trig.cos(SCALE + PI / 4), 707106781186547600, TOL); 210 | } 211 | function testCos14() public { 212 | assertApproxEqRel(trig.cos(SCALE + PI / 2), 0, TOL); 213 | } 214 | function testCos15() public { 215 | assertApproxEqRel(trig.cos(SCALE + PI * 3 / 4), -707106781186547600, TOL); 216 | } 217 | function testCos16() public { 218 | assertApproxEqRel(trig.cos(SCALE + PI), -1e18, TOL); 219 | } 220 | function testCos17() public { 221 | assertApproxEqRel(trig.cos(SCALE + PI * 5 / 4), -707106781186547600, TOL); 222 | } 223 | function testCos18() public { 224 | assertApproxEqRel(trig.cos(SCALE + PI * 3 / 2), 0, TOL); 225 | } 226 | function testCos19() public { 227 | assertApproxEqRel(trig.cos(SCALE + PI * 7 / 4), 707106781186547600, TOL); 228 | } 229 | function testCos20() public { 230 | assertApproxEqRel(trig.cos(SCALE + PI * 2), 1e18, TOL); 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /test/trig.py: -------------------------------------------------------------------------------- 1 | """ 2 | Calculates the sine or cosine of the provided number. 3 | The value is in radians scaled by 1e18, e.g. use a value of 1e18 for 1 radian. 4 | 5 | usage: python trig.py 6 | """ 7 | 8 | import subprocess, sys 9 | from decimal import Decimal 10 | from math import sin, cos, asin 11 | 12 | # Parse arguments 13 | if len(sys.argv) != 3: 14 | raise Exception("Must pass a method name and value") 15 | 16 | method = sys.argv[1] 17 | if method not in ["sin", "cos", "arcsin"]: 18 | raise Exception("Method must be sin, cos, or arcsin") 19 | 20 | raw_input = Decimal(sys.argv[2]) 21 | 22 | if method in ["sin", "cos"]: 23 | raw_angle = raw_input 24 | if raw_angle < 0: 25 | raise Exception("Angle must be positive") 26 | 27 | angle = raw_angle / (10 ** 18) 28 | x = sin(angle) if method == "sin" else cos(angle) 29 | else: 30 | arg = raw_input / (10 ** 18) 31 | x = asin(arg) 32 | 33 | # Convert back to an integer scaled by 1e18, then ABI encode the result 34 | y = int(x * 10 ** 18) 35 | subprocess.run(["cast", "abi-encode", "f(int256)", str(y)]) 36 | --------------------------------------------------------------------------------