diff --git a/.env.example b/.env.example index 1441dbbd..606f2ae6 100644 --- a/.env.example +++ b/.env.example @@ -22,3 +22,6 @@ VAULT_BATCH_TEST_AVALANCHE_BLOCK= LENDING_BATCH_TEST_SONIC_BLOCK= FOUNDRY_PROFILE=lite + +# Address of delegator required to deploy LZ-bridges +LZ_DELEGATOR= \ No newline at end of file diff --git a/.github/workflows/fmt.yml b/.github/workflows/fmt.yml index 640f46c8..eb69ea03 100644 --- a/.github/workflows/fmt.yml +++ b/.github/workflows/fmt.yml @@ -18,7 +18,7 @@ jobs: name: Formatter runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: submodules: recursive diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b29b214d..9861be58 100755 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,9 +18,17 @@ jobs: name: Test, coverage runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: submodules: recursive + fetch-depth: 0 + fetch-tags: true + + - name: Ensure submodules are on correct commits + run: | + git submodule sync --recursive + git submodule update --init --recursive + git submodule status - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 @@ -74,7 +82,7 @@ jobs: id: coverage - name: Upload coverage lcov report to Codecov - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 env: CODECOV_TOKEN: ${{secrets.CODECOV_TOKEN}} diff --git a/.gitmodules b/.gitmodules index e91d3a25..55c08ba1 100644 --- a/.gitmodules +++ b/.gitmodules @@ -10,3 +10,12 @@ [submodule "lib/openzeppelin-contracts"] path = lib/openzeppelin-contracts url = https://github.com/OpenZeppelin/openzeppelin-contracts +[submodule "lib/devtools"] + path = lib/devtools + url = https://github.com/layerzero-labs/devtools +[submodule "lib/LayerZero-v2"] + path = lib/LayerZero-v2 + url = https://github.com/layerzero-labs/LayerZero-v2 +[submodule "lib/solidity-bytes-utils"] + path = lib/solidity-bytes-utils + url = https://github.com/GNSPS/solidity-bytes-utils.git diff --git a/chains/avalanche/AvalancheConstantsLib.sol b/chains/avalanche/AvalancheConstantsLib.sol index aafa939b..0b7ba974 100644 --- a/chains/avalanche/AvalancheConstantsLib.sol +++ b/chains/avalanche/AvalancheConstantsLib.sol @@ -64,6 +64,21 @@ library AvalancheConstantsLib { address public constant SILO_MANAGED_VAULT_AUSD_VARLAMOURE = 0x3d7B0c3997E48fA3FC96cd057d1fb4E5F891835B; address public constant SILO_MANAGED_VAULT_USDt_VARLAMOURE = 0x6c09bfdc1df45D6c4Ff78Dc9F1C13aF29eB335d4; + // ---------------------------------- LayerZero-v2 https://docs.layerzero.network/v2/deployments/chains/avalanche + uint32 public constant LAYER_ZERO_V2_ENDPOINT_ID = 30106; + address public constant LAYER_ZERO_V2_ENDPOINT = 0x1a44076050125825900e736c501f859c50fE728c; + address public constant LAYER_ZERO_V2_SEND_ULN_302 = 0x197D1333DEA5Fe0D6600E9b396c7f1B1cFCc558a; + address public constant LAYER_ZERO_V2_RECEIVE_ULN_302 = 0xbf3521d309642FA9B1c91A08609505BA09752c61; + address public constant LAYER_ZERO_V2_READ_LIB_1002 = 0x8839D3f169f473193423b402BDC4B5c51daAABDc; + address public constant LAYER_ZERO_V2_EXECUTOR = 0x90E595783E43eb89fF07f63d27B8430e6B44bD9c; + address public constant LAYER_ZERO_V2_BLOCKED_MESSAGE_LIBRARY = 0x1ccBf0db9C192d969de57E25B3fF09A25bb1D862; + address public constant LAYER_ZERO_V2_DEAD_DVN = 0x90cCA24D1338Bd284C25776D9c12f96764Bde5e1; + + // https://docs.layerzero.network/v2/deployments/chains/avalanche + address internal constant AVALANCHE_DVN_LAYER_ZERO_PULL = 0x0Ffe02DF012299A370D5dd69298A5826EAcaFdF8; // LayerZero Labs (lzRead) + address internal constant AVALANCHE_DVN_LAYER_ZERO_PUSH = 0x962F502A63F5FBeB44DC9ab932122648E8352959; + address internal constant AVALANCHE_DVN_NETHERMIND_PULL = 0x1308151a7ebaC14f435d3Ad5fF95c34160D539A5; // Nethermind (lzRead) + address internal constant AVALANCHE_DVN_HORIZON_PULL = 0x1a5Df1367F21d55B13D5E2f8778AD644BC97aC6d; // Horizen (lzRead) // DeX aggregators /// @notice Aggregator router V6 diff --git a/chains/plasma/PlasmaConstantsLib.sol b/chains/plasma/PlasmaConstantsLib.sol index 9cdbfb79..f908eb87 100644 --- a/chains/plasma/PlasmaConstantsLib.sol +++ b/chains/plasma/PlasmaConstantsLib.sol @@ -43,6 +43,7 @@ library PlasmaConstantsLib { address public constant AAVE_V3_POOL_DATA_PROVIDER = 0xf2D6E38B407e31E7E7e4a16E6769728b76c7419F; address public constant AAVE_V3_POOL = 0x925a2A7214Ed92428B5b1B090F80b25700095e12; address public constant AAVE_V3_POOL_USDT0 = 0x5D72a9d9A9510Cd8cBdBA12aC62593A58930a948; + address public constant AAVE_V3_POOL_WETH = 0xf1aB7f60128924d69f6d7dE25A20eF70bBd43d07; address public constant AAVE_V3_POOL_SUSDE = 0xC1A318493fF07a68fE438Cee60a7AD0d0DBa300E; address public constant AAVE_V3_POOL_WEETH = 0xAf1a7a488c8348b41d5860C04162af7d3D38A996; @@ -63,4 +64,18 @@ library PlasmaConstantsLib { address public constant POOL_SOLIDLY_WETH_WEETH = 0x355705857c9548E71E866087b01bB5b0A1fd671b; + // ---------------------------------- LayerZero-v2 https://docs.layerzero.network/v2/deployments/chains/plasma + uint32 public constant LAYER_ZERO_V2_ENDPOINT_ID = 30383; + address public constant LAYER_ZERO_V2_ENDPOINT = 0x6F475642a6e85809B1c36Fa62763669b1b48DD5B; + address public constant LAYER_ZERO_V2_SEND_ULN_302 = 0xC39161c743D0307EB9BCc9FEF03eeb9Dc4802de7; + address public constant LAYER_ZERO_V2_RECEIVE_ULN_302 = 0xe1844c5D63a9543023008D332Bd3d2e6f1FE1043; + address public constant LAYER_ZERO_V2_READ_LIB_1002 = 0x860E8D714944E7accE4F9e6247923ec5d30c0471; + address public constant LAYER_ZERO_V2_EXECUTOR = 0x4208D6E27538189bB48E603D6123A94b8Abe0A0b; + address public constant LAYER_ZERO_V2_BLOCKED_MESSAGE_LIBRARY = 0xC1cE56B2099cA68720592583C7984CAb4B6d7E7a; + address public constant LAYER_ZERO_V2_DEAD_DVN = 0x6788f52439ACA6BFF597d3eeC2DC9a44B8FEE842; + + // https://docs.layerzero.network/v2/deployments/chains/plasma + address internal constant PLASMA_DVN_LAYER_ZERO_PUSH = 0x282b3386571f7f794450d5789911a9804FA346b4; // LayerZero Labs (push based) + address internal constant PLASMA_DVN_NETHERMIND_PUSH = 0xa51cE237FaFA3052D5d3308Df38A024724Bb1274; // Nethermind (push based) + address internal constant PLASMA_DVN_HORIZON_PUSH = 0xd4CE45957FBCb88b868ad2c759C7DB9BC2741e56; // Horizen (push based) } diff --git a/chains/sonic/SonicConstantsLib.sol b/chains/sonic/SonicConstantsLib.sol index d6956cfa..3612c1d6 100644 --- a/chains/sonic/SonicConstantsLib.sol +++ b/chains/sonic/SonicConstantsLib.sol @@ -603,4 +603,21 @@ library SonicConstantsLib { // ---------------------------------- Mainstreet address internal constant MSUSD_MINTER = 0xb1E423c251E989bd4e49228eF55aC4747D63F54D; + // ---------------------------------- LayerZero-v2 https://docs.layerzero.network/v2/deployments/chains/sonic + uint32 public constant LAYER_ZERO_V2_ENDPOINT_ID = 30332; + address public constant LAYER_ZERO_V2_ENDPOINT = 0x6F475642a6e85809B1c36Fa62763669b1b48DD5B; + address public constant LAYER_ZERO_V2_SEND_ULN_302 = 0xC39161c743D0307EB9BCc9FEF03eeb9Dc4802de7; + address public constant LAYER_ZERO_V2_RECEIVE_ULN_302 = 0xe1844c5D63a9543023008D332Bd3d2e6f1FE1043; + address public constant LAYER_ZERO_V2_READ_LIB_1002 = 0x860E8D714944E7accE4F9e6247923ec5d30c0471; + address public constant LAYER_ZERO_V2_EXECUTOR = 0x4208D6E27538189bB48E603D6123A94b8Abe0A0b; + address public constant LAYER_ZERO_V2_BLOCKED_MESSAGE_LIBRARY = 0xC1cE56B2099cA68720592583C7984CAb4B6d7E7a; + address public constant LAYER_ZERO_V2_DEAD_DVN = 0x6788f52439ACA6BFF597d3eeC2DC9a44B8FEE842; + + // https://docs.layerzero.network/v2/deployments/chains/sonic + address internal constant SONIC_DVN_NETHERMIND_PULL = 0x3b0531eB02Ab4aD72e7a531180beeF9493a00dD2; // Nethermind (lzRead) + address internal constant SONIC_DVN_LAYER_ZERO_PULL = 0x78f607fc38e071cEB8630B7B12c358eE01C31E96; // LayerZero Labs (lzRead) + address internal constant SONIC_DVN_LAYER_ZERO_PUSH = 0x282b3386571f7f794450d5789911a9804FA346b4; + address internal constant SONIC_DVN_HORIZEN_PUSH = 0x54dD79f5cE72b51FCBbcb170Dd01E32034323565; + address internal constant SONIC_DVN_HORIZEN_PULL = 0xCA764b512E2d2fD15fcA1c0a38F7cFE9153148F0; // Horizen (lzRead) + } \ No newline at end of file diff --git a/config.d.toml b/config.d.toml new file mode 100644 index 00000000..7fdbc924 --- /dev/null +++ b/config.d.toml @@ -0,0 +1,18 @@ +[sonic.address] +xToken = "0x902215dd96a291b256a3Aef6c4Dee62d2A9B80Cb" +xStaking = "0x17a7Cf838A7C91DE47552a9f65822B547F9A6997" +DAO = "0x77773Cb473aD1bfE991bA299a127F64b45C17777" +PRICE_AGGREGATOR_OAPP_MAIN_TOKEN = "0x61752dB3C155f73Cc7Dda1e70d065b804Bce5e9B" +OAPP_MAIN_TOKEN = "0xD6a8b05f08834Ed2f205E3d591CD6D1A84b7C19B" +XTokenBridge = "0x533A0c7869e36D1640D4058Bac4604DB6b4d7AD5" + +[9745.address] +BRIDGED_PRICE_ORACLE_MAIN_TOKEN = "0x1984C54B371273Ba37030e56121Cd9d18537b2D6" +OAPP_MAIN_TOKEN = "0xfdf91362B7E9330F232e500c0236a02B0DE3e492" +xToken = "0xF40D0724599282CaF9dfb66feB630e936bC0CFBE" +xStaking = "0x601572b91DC054Be500392A6d3e15c690140998D" +DAO = "0x87C51aa090587790A5298ea4C2d0DBbcCD0026A6" +XTokenBridge = "0x4E3F0A27bbF443Ba81FCf17E28F4100f35b1b51B" +recoveryRelayer = "0x046e7a007C331e0d4DafA66104744dB14a52bBBb" + +[avalanche.address] diff --git a/config.toml b/config.toml new file mode 100644 index 00000000..d8993251 --- /dev/null +++ b/config.toml @@ -0,0 +1,43 @@ +# -------------------------------------------------- Sonic +[sonic] + +[sonic.address] +PLATFORM = "0x4Aca671A420eEB58ecafE83700686a2AD06b20D8" +MULTISIG = "0xF564EBaC1182578398E94868bea1AbA6ba339652" + +TOKEN_USDC = "0x29219dd400f2Bf60E5a23d13Be72B486D4038894" +TOKEN_STBL = "0x78a76316F66224CBaCA6e70acB24D5ee5b2Bd2c7" + +LAYER_ZERO_V2_ENDPOINT = "0x6F475642a6e85809B1c36Fa62763669b1b48DD5B" + +[sonic.uint] +LAYER_ZERO_V2_ENDPOINT_ID = 30332 + +# -------------------------------------------------- 9745 (Plasma) +[9745] + +[9745.address] +PLATFORM = "0xd4D6ad656f64E8644AFa18e7CCc9372E0Cd256f0" +MULTISIG = "0xE929438B5B53984FdBABf8562046e141e90E8099" + +TOKEN_USDT0 = "0xB8CE59FC3717ada4C02eaDF9682A9e934F625ebb" + +LAYER_ZERO_V2_ENDPOINT = "0x6F475642a6e85809B1c36Fa62763669b1b48DD5B" + +[9745.uint] +LAYER_ZERO_V2_ENDPOINT_ID = 30383 + +# -------------------------------------------------- Avalanche +[avalanche] + +[avalanche.address] +PLATFORM = "0x72b931a12aaCDa6729b4f8f76454855CB5195941" +MULTISIG = "0x06111E02BEb85B57caebEf15F5f90Bc82D54da3A" + +TOKEN_USDC = "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E" +TOKEN_USDT = "0x9702230A8Ea53601f5cD2dc00fDBc13d4dF4A8c7" + +LAYER_ZERO_V2_ENDPOINT = "0x1a44076050125825900e736c501f859c50fE728c" + +[avalanche.uint] +LAYER_ZERO_V2_ENDPOINT_ID = 30106 diff --git a/foundry.lock b/foundry.lock index 364524bc..6022c5b9 100644 --- a/foundry.lock +++ b/foundry.lock @@ -1,14 +1,35 @@ { + "lib/LayerZero-v2": { + "rev": "3801b9929281261b907eb3482a82364ad00d7868" + }, + "lib/devtools": { + "rev": "77ba15585252d58e1f613d69bef5013b15be0240" + }, "lib/forge-std": { - "rev": "1eea5bae12ae557d589f9f0f0edae2faa47cb262" + "tag": { + "name": "v1.11.0", + "rev": "8e40513d678f392f398620b3ef2b418648b33e89" + } }, "lib/openzeppelin-contracts": { - "rev": "932fddf69a699a9a80fd2396fd1a2ab91cdda123" + "tag": { + "name": "v5.4.0", + "rev": "c64a1edb67b6e3f4a15cca8909c9482ad33a02b0" + } }, "lib/openzeppelin-contracts-upgradeable": { - "rev": "625fb3c2b2696f1747ba2e72d1e1113066e6c177" + "tag": { + "name": "v5.4.0", + "rev": "e725abddf1e01cf05ace496e950fc8e243cc7cab" + } }, "lib/solady": { - "rev": "fe918e7d7b560dee66e657f49ef75645ec10f2e4" + "tag": { + "name": "v0.1.26", + "rev": "acd959aa4bd04720d640bf4e6a5c71037510cc4b" + } + }, + "lib/solidity-bytes-utils": { + "rev": "fc502455bb2a7e26a743378df042612dd50d1eb9" } } \ No newline at end of file diff --git a/foundry.toml b/foundry.toml index 0712a2ed..d7b15517 100644 --- a/foundry.toml +++ b/foundry.toml @@ -30,8 +30,10 @@ arbitrum = "${ARBITRUM_RPC_URL}" ethereum = "${ETHEREUM_RPC_URL}" real = "${REAL_RPC_URL}" sonic = "${SONIC_RPC_URL}" +146 = "${SONIC_RPC_URL}" # required for StdConfig avalanche = "${AVALANCHE_RPC_URL}" plasma = "${PLASMA_RPC_URL}" +9745 = "${PLASMA_RPC_URL}" # required for StdConfig [etherscan] polygon = { key = "${POLYGONSCAN_API_KEY}", chain = 137 } diff --git a/lib/LayerZero-v2 b/lib/LayerZero-v2 new file mode 160000 index 00000000..3801b992 --- /dev/null +++ b/lib/LayerZero-v2 @@ -0,0 +1 @@ +Subproject commit 3801b9929281261b907eb3482a82364ad00d7868 diff --git a/lib/devtools b/lib/devtools new file mode 160000 index 00000000..77ba1558 --- /dev/null +++ b/lib/devtools @@ -0,0 +1 @@ +Subproject commit 77ba15585252d58e1f613d69bef5013b15be0240 diff --git a/lib/solidity-bytes-utils b/lib/solidity-bytes-utils new file mode 160000 index 00000000..fc502455 --- /dev/null +++ b/lib/solidity-bytes-utils @@ -0,0 +1 @@ +Subproject commit fc502455bb2a7e26a743378df042612dd50d1eb9 diff --git a/remappings.txt b/remappings.txt index 1a21f9fb..05dc82f5 100644 --- a/remappings.txt +++ b/remappings.txt @@ -8,3 +8,10 @@ openzeppelin-contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/ openzeppelin/=lib/openzeppelin-contracts-upgradeable/contracts/ solady/=lib/solady/ openzeppelin-contracts/=lib/openzeppelin-contracts/ +@layerzerolabs/lz-evm-protocol-v2/=lib/LayerZero-v2/packages/layerzero-v2/evm/protocol +@layerzerolabs/lz-evm-messagelib-v2/=lib/LayerZero-v2/packages/layerzero-v2/evm/messagelib +@layerzerolabs/oapp-evm/=lib/devtools/packages/oapp-evm/ +@layerzerolabs/oft-evm/=lib/devtools/packages/oft-evm/ +@layerzerolabs/oft-evm-upgradeable/=lib/devtools/packages/oft-evm-upgradeable/ +@layerzerolabs/oapp-evm-upgradeable/=lib/devtools/packages/oapp-evm-upgradeable/ +solidity-bytes-utils/=lib/solidity-bytes-utils/ \ No newline at end of file diff --git a/script/deploy-periphery/BridgedPriceOracle.s.sol b/script/deploy-periphery/BridgedPriceOracle.s.sol new file mode 100644 index 00000000..86c3ac60 --- /dev/null +++ b/script/deploy-periphery/BridgedPriceOracle.s.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {StdConfig} from "forge-std/StdConfig.sol"; +import {Variable, LibVariable} from "forge-std/LibVariable.sol"; +import {Script} from "forge-std/Script.sol"; +import {Proxy} from "../../src/core/proxy/Proxy.sol"; +import {BridgedPriceOracle} from "../../src/periphery/BridgedPriceOracle.sol"; + +contract DeployBridgedPriceOracle is Script { + using LibVariable for Variable; + + function run() external { + uint deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + address delegator = vm.envAddress("LZ_DELEGATOR"); + require(delegator != address(0), "delegator is not set"); + + // ---------------------- Initialize + StdConfig config = new StdConfig("./config.toml", false); // read only config + StdConfig configDeployed = new StdConfig("./config.d.toml", true); // auto-write deployed addresses + + require( + uint(configDeployed.get("BRIDGED_PRICE_ORACLE_MAIN_TOKEN").ty.kind) == 0, + "BridgedPriceOracle already deployed" + ); + + // ---------------------- Deploy + vm.startBroadcast(deployerPrivateKey); + Proxy proxy = new Proxy(); + { + address implementation = address(new BridgedPriceOracle(config.get("LAYER_ZERO_V2_ENDPOINT").toAddress())); + proxy.initProxy(implementation); + require(proxy.implementation() == implementation, "BridgedPriceOracle: implementation mismatch"); + } + + // @dev assume here that we deploy price oracle for STBL token + BridgedPriceOracle(address(proxy)).initialize(config.get("PLATFORM").toAddress(), "STBL", delegator); + + // ---------------------- Write results + vm.stopBroadcast(); + + // @dev assume here that we deploy price oracle for STBL token + configDeployed.set("BRIDGED_PRICE_ORACLE_MAIN_TOKEN", address(proxy)); + } + + function testDeployScript() external {} +} diff --git a/script/deploy-periphery/PriceAggregatorOApp.Sonic.s.sol b/script/deploy-periphery/PriceAggregatorOApp.Sonic.s.sol new file mode 100644 index 00000000..40dcbd69 --- /dev/null +++ b/script/deploy-periphery/PriceAggregatorOApp.Sonic.s.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {StdConfig} from "forge-std/StdConfig.sol"; +import {Variable, LibVariable} from "forge-std/LibVariable.sol"; +import {Script} from "forge-std/Script.sol"; +import {Proxy} from "../../src/core/proxy/Proxy.sol"; +import {PriceAggregatorOApp} from "../../src/periphery/PriceAggregatorOApp.sol"; + +contract DeployPriceAggregatorOAppSonic is Script { + using LibVariable for Variable; + + function run() external { + uint deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + address delegator = vm.envAddress("LZ_DELEGATOR"); + require(delegator != address(0), "delegator is not set"); + + // ---------------------- Initialize + StdConfig config = new StdConfig("./config.toml", false); // read only config + StdConfig configDeployed = new StdConfig("./config.d.toml", true); // auto-write deployed addresses + + require( + block.chainid == 146, + "PriceAggregatorOApp is used on the Sonic only (the chain where native STBL is deployed)" + ); + require( + uint(configDeployed.get("PRICE_AGGREGATOR_OAPP_MAIN_TOKEN").ty.kind) == 0, + "PriceAggregatorOApp already deployed" + ); + + // ---------------------- Deploy + vm.startBroadcast(deployerPrivateKey); + Proxy proxy = new Proxy(); + { + address implementation = address(new PriceAggregatorOApp(config.get("LAYER_ZERO_V2_ENDPOINT").toAddress())); + proxy.initProxy(implementation); + require(proxy.implementation() == implementation, "PriceAggregatorOApp: implementation mismatch"); + } + + // @dev assume here that we deploy price oracle for STBL token + PriceAggregatorOApp(address(proxy)) + .initialize(config.get("PLATFORM").toAddress(), config.get("TOKEN_STBL").toAddress(), delegator); + + // ---------------------- Write results + vm.stopBroadcast(); + + // @dev assume here that we deploy price oracle for main-token + configDeployed.set("PRICE_AGGREGATOR_OAPP_MAIN_TOKEN", address(proxy)); + } + + function testDeployScript() external {} +} diff --git a/script/deploy-tokenomics/BridgedToken.Upgrade.s.sol b/script/deploy-tokenomics/BridgedToken.Upgrade.s.sol new file mode 100644 index 00000000..aaff4d78 --- /dev/null +++ b/script/deploy-tokenomics/BridgedToken.Upgrade.s.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {StdConfig} from "forge-std/StdConfig.sol"; +import {Variable, LibVariable} from "forge-std/LibVariable.sol"; +import {Script} from "forge-std/Script.sol"; +import {BridgedToken} from "../../src/tokenomics/BridgedToken.sol"; + +contract DeployBridgedTokenImplementation is Script { + using LibVariable for Variable; + + function run() external { + uint deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + + StdConfig config = new StdConfig("./config.toml", false); // read only config + + vm.startBroadcast(deployerPrivateKey); + new BridgedToken(config.get("LAYER_ZERO_V2_ENDPOINT").toAddress()); + + vm.stopBroadcast(); + } + + function testDeployScript() external {} +} diff --git a/script/deploy-tokenomics/BridgedToken.s.sol b/script/deploy-tokenomics/BridgedToken.s.sol new file mode 100644 index 00000000..61031888 --- /dev/null +++ b/script/deploy-tokenomics/BridgedToken.s.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {StdConfig} from "forge-std/StdConfig.sol"; +import {Variable, LibVariable} from "forge-std/LibVariable.sol"; +import {Script} from "forge-std/Script.sol"; +import {Proxy} from "../../src/core/proxy/Proxy.sol"; +import {BridgedToken} from "../../src/tokenomics/BridgedToken.sol"; + +contract DeployBridgedToken is Script { + using LibVariable for Variable; + + function run() external { + uint deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + address delegator = vm.envAddress("LZ_DELEGATOR"); + require(delegator != address(0), "delegator is not set"); + + // ---------------------- Initialize + StdConfig config = new StdConfig("./config.toml", false); // read only config + StdConfig configDeployed = new StdConfig("./config.d.toml", true); // auto-write deployed addresses + + require( + uint(configDeployed.get("OAPP_MAIN_TOKEN").ty.kind) == 0, + "TokenOFTAdapter is already deployed on this chain" + ); + require(uint(config.get("LAYER_ZERO_V2_ENDPOINT").ty.kind) != 0, "endpoint is not set"); + require(uint(config.get("PLATFORM").ty.kind) != 0, "platform is not set"); + + address endpoint = config.get("LAYER_ZERO_V2_ENDPOINT").toAddress(); + address platform = config.get("PLATFORM").toAddress(); + + // ---------------------- Deploy + vm.startBroadcast(deployerPrivateKey); + Proxy proxy = new Proxy(); + proxy.initProxy(address(new BridgedToken(endpoint))); + BridgedToken(address(proxy)).initialize(platform, "Stability", "STBL", delegator); + + // ---------------------- Write results + vm.stopBroadcast(); + + configDeployed.set("OAPP_MAIN_TOKEN", address(proxy)); + } + + function testDeployScript() external {} +} diff --git a/script/deploy-tokenomics/DAO.s.sol b/script/deploy-tokenomics/DAO.s.sol new file mode 100644 index 00000000..d8f0d4dc --- /dev/null +++ b/script/deploy-tokenomics/DAO.s.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import {DAO, IDAO} from "../../src/tokenomics/DAO.sol"; +import {Proxy} from "../../src/core/proxy/Proxy.sol"; +import {Script} from "forge-std/Script.sol"; +import {StdConfig} from "forge-std/StdConfig.sol"; + +contract DeployDAO is Script { + uint internal constant SONIC_CHAIN_ID = 146; + + function run() external { + uint deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + + // ---------------------- Initialize + StdConfig config = new StdConfig("./config.toml", false); // read only config + StdConfig configDeployed = new StdConfig("./config.d.toml", true); // auto-write deployed addresses + + require(uint(configDeployed.get("xToken").ty.kind) != 0, "xToken is not deployed on the chain"); + require(uint(configDeployed.get("xStaking").ty.kind) != 0, "xStaking is not deployed on the chain"); + + address xToken = configDeployed.get("xToken").toAddress(); + address xStaking = configDeployed.get("xStaking").toAddress(); + + require(uint(config.get("PLATFORM").ty.kind) != 0, "Platform is not deployed on the chain"); + address platform = config.get("PLATFORM").toAddress(); + + require(uint(configDeployed.get("DAO").ty.kind) == 0, "DAO is already deployed on the chain"); + + IDAO.DaoParams memory params = IDAO.DaoParams({ + minimalPower: 4000000000000000000000, + exitPenalty: 8000, + proposalThreshold: 10000, + quorum: 30000, + powerAllocationDelay: 86400 + }); + + // ---------------------- Deploy + vm.startBroadcast(deployerPrivateKey); + + Proxy daoProxy = new Proxy(); + { + address implementation = address(new DAO()); + daoProxy.initProxy(implementation); + require(daoProxy.implementation() == implementation, "DAO: implementation mismatch"); + } + + DAO(address(daoProxy)).initialize(platform, xToken, address(xStaking), params, "Stability DAO", "STBL_DAO"); + + // let's try to use Snapshot delegation + // todo DAO(address(daoProxy)).setDelegationForbidden(true); + + // ---------------------- Write results + vm.stopBroadcast(); + + configDeployed.set("DAO", address(daoProxy)); + } + + function testDeployScript() external {} +} diff --git a/script/deploy-tokenomics/RecoveryRelayer.s.sol b/script/deploy-tokenomics/RecoveryRelayer.s.sol new file mode 100644 index 00000000..a7f146cc --- /dev/null +++ b/script/deploy-tokenomics/RecoveryRelayer.s.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import {StdConfig} from "forge-std/StdConfig.sol"; +import {Script} from "forge-std/Script.sol"; +import {Proxy} from "../../src/core/proxy/Proxy.sol"; +import {RecoveryRelayer} from "../../src/tokenomics/RecoveryRelayer.sol"; +// import {console} from "forge-std/console.sol"; + +contract DeployRecoveryRelayer is Script { + uint internal constant SONIC_CHAIN_ID = 146; + + function run() external { + uint deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + + // ---------------------- Initialize + StdConfig config = new StdConfig("./config.toml", false); // read only config + StdConfig configDeployed = new StdConfig("./config.d.toml", true); // auto-write deployed addresses + + require( + block.chainid != SONIC_CHAIN_ID, + "Recovery is used on Sonic instead of RecoveryRelayer, deploy is not allowed" + ); + + // todo how to implement such check? + // require( + // uint(configDeployed.get("recoveryRelayer").ty.kind) == 0, "recoveryRelayer is already deployed on the chain" + // ); + + require(uint(config.get("PLATFORM").ty.kind) != 0, "Platform is not deployed on the chain"); + address platform = config.get("PLATFORM").toAddress(); + + // ---------------------- Deploy + vm.startBroadcast(deployerPrivateKey); + + Proxy proxy = new Proxy(); + { + address implementation = address(new RecoveryRelayer()); + proxy.initProxy(implementation); + require(proxy.implementation() == implementation, "RecoveryRelayer: implementation mismatch"); + } + + RecoveryRelayer(address(proxy)).initialize(platform); + + // ---------------------- Write results + vm.stopBroadcast(); + + configDeployed.set("recoveryRelayer", address(proxy)); + } + + function testDeployScript() external {} +} diff --git a/script/deploy-tokenomics/TokenOFTAdapter.Sonic.s.sol b/script/deploy-tokenomics/TokenOFTAdapter.Sonic.s.sol new file mode 100644 index 00000000..925780d2 --- /dev/null +++ b/script/deploy-tokenomics/TokenOFTAdapter.Sonic.s.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {StdConfig} from "forge-std/StdConfig.sol"; +import {Variable, LibVariable} from "forge-std/LibVariable.sol"; +import {Script} from "forge-std/Script.sol"; +import {Proxy} from "../../src/core/proxy/Proxy.sol"; +import {TokenOFTAdapter} from "../../src/tokenomics/TokenOFTAdapter.sol"; + +contract DeployTokenOFTAdapterSonic is Script { + using LibVariable for Variable; + + function run() external { + uint deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + address delegator = vm.envAddress("LZ_DELEGATOR"); + require(delegator != address(0), "delegator is not set"); + + // ---------------------- Initialize + StdConfig config = new StdConfig("./config.toml", false); // read only config + StdConfig configDeployed = new StdConfig("./config.d.toml", true); // auto-write deployed addresses + + require( + block.chainid == 146, "TokenOFTAdapter is used on the Sonic only (the chain where native STBL is deployed)" + ); + + require( + uint(configDeployed.get("OAPP_MAIN_TOKEN").ty.kind) == 0, "TokenOFTAdapter is already deployed on Sonic" + ); + require(uint(config.get("LAYER_ZERO_V2_ENDPOINT").ty.kind) != 0, "endpoint is not set"); + require(uint(config.get("PLATFORM").ty.kind) != 0, "platform is not set"); + + // ---------------------- Deploy + vm.startBroadcast(deployerPrivateKey); + Proxy proxy = new Proxy(); + proxy.initProxy( + address( + new TokenOFTAdapter( + config.get("TOKEN_STBL").toAddress(), config.get("LAYER_ZERO_V2_ENDPOINT").toAddress() + ) + ) + ); + TokenOFTAdapter(address(proxy)).initialize(config.get("PLATFORM").toAddress(), delegator); + + // ---------------------- Write results + vm.stopBroadcast(); + + configDeployed.set("OAPP_MAIN_TOKEN", address(proxy)); + } + + function testDeployScript() external {} +} diff --git a/script/deploy-tokenomics/XSTBL.Sonic.s.sol b/script/deploy-tokenomics/XSTBL.Sonic.s.sol index 83d08862..9bfc3f81 100644 --- a/script/deploy-tokenomics/XSTBL.Sonic.s.sol +++ b/script/deploy-tokenomics/XSTBL.Sonic.s.sol @@ -4,12 +4,12 @@ pragma solidity ^0.8.28; import {Script} from "forge-std/Script.sol"; import {Proxy} from "../../src/core/proxy/Proxy.sol"; import {XStaking} from "../../src/tokenomics/XStaking.sol"; -import {XSTBL} from "../../src/tokenomics/XSTBL.sol"; +import {XToken} from "../../src/tokenomics/XToken.sol"; import {RevenueRouter} from "../../src/tokenomics/RevenueRouter.sol"; import {FeeTreasury} from "../../src/tokenomics/FeeTreasury.sol"; import {IPlatform} from "../../src/interfaces/IPlatform.sol"; -contract DeployXSTBLSystem is Script { +contract DeployXTokenSystemSonic is Script { address public constant PLATFORM = 0x4Aca671A420eEB58ecafE83700686a2AD06b20D8; address public constant STBL = 0x78a76316F66224CBaCA6e70acB24D5ee5b2Bd2c7; @@ -19,14 +19,15 @@ contract DeployXSTBLSystem is Script { Proxy xStakingProxy = new Proxy(); xStakingProxy.initProxy(address(new XStaking())); Proxy xSTBLProxy = new Proxy(); - xSTBLProxy.initProxy(address(new XSTBL())); + xSTBLProxy.initProxy(address(new XToken())); Proxy revenueRouterProxy = new Proxy(); revenueRouterProxy.initProxy(address(new RevenueRouter())); Proxy feeTreasuryProxy = new Proxy(); feeTreasuryProxy.initProxy(address(new FeeTreasury())); FeeTreasury(address(feeTreasuryProxy)).initialize(PLATFORM, IPlatform(PLATFORM).multisig()); XStaking(address(xStakingProxy)).initialize(PLATFORM, address(xSTBLProxy)); - XSTBL(address(xSTBLProxy)).initialize(PLATFORM, STBL, address(xStakingProxy), address(revenueRouterProxy)); + XToken(address(xSTBLProxy)) + .initialize(PLATFORM, STBL, address(xStakingProxy), address(revenueRouterProxy), "xStability", "xSTBL"); RevenueRouter(address(revenueRouterProxy)).initialize(PLATFORM, address(xSTBLProxy), address(feeTreasuryProxy)); vm.stopBroadcast(); } diff --git a/script/deploy-tokenomics/XToken.s.sol b/script/deploy-tokenomics/XToken.s.sol new file mode 100644 index 00000000..248a1e63 --- /dev/null +++ b/script/deploy-tokenomics/XToken.s.sol @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import {StdConfig} from "forge-std/StdConfig.sol"; +import {Script} from "forge-std/Script.sol"; +import {Proxy} from "../../src/core/proxy/Proxy.sol"; +import {XStaking} from "../../src/tokenomics/XStaking.sol"; +import {XToken} from "../../src/tokenomics/XToken.sol"; +import {IPlatform} from "../../src/interfaces/IPlatform.sol"; + +contract DeployXTokenSystem is Script { + uint internal constant SONIC_CHAIN_ID = 146; + + function run() external { + uint deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + + // ---------------------- Initialize + StdConfig config = new StdConfig("./config.toml", false); // read only config + StdConfig configDeployed = new StdConfig("./config.d.toml", true); // auto-write deployed addresses + + // Native STBL is deployed on Sonic. All other chains use bridged versions of STBL + if (block.chainid != SONIC_CHAIN_ID) { + require(uint(configDeployed.get("OAPP_MAIN_TOKEN").ty.kind) != 0, "Main token is not deployed on the chain"); + } + address mainToken = block.chainid == SONIC_CHAIN_ID + ? config.get("TOKEN_STBL").toAddress() + : configDeployed.get("OAPP_MAIN_TOKEN").toAddress(); + + require(uint(config.get("PLATFORM").ty.kind) != 0, "Platform is not deployed on the chain"); + address platform = config.get("PLATFORM").toAddress(); + + address revenueRouter = address(IPlatform(platform).revenueRouter()); + require(revenueRouter != address(0), "RevenueRouter address is zero"); + + require(uint(configDeployed.get("xToken").ty.kind) == 0, "xToken is already deployed on the chain"); + require(uint(configDeployed.get("xStaking").ty.kind) == 0, "xStaking is already deployed on the chain"); + + // ---------------------- Deploy + vm.startBroadcast(deployerPrivateKey); + + Proxy xStakingProxy = new Proxy(); + { + address implementation = address(new XStaking()); + xStakingProxy.initProxy(implementation); + require(xStakingProxy.implementation() == implementation, "XStaking: implementation mismatch"); + } + + Proxy xSTBLProxy = new Proxy(); + { + address implementation = address(new XToken()); + xSTBLProxy.initProxy(implementation); + require(xSTBLProxy.implementation() == implementation, "XToken: implementation mismatch"); + } + + XStaking(address(xStakingProxy)).initialize(platform, address(xSTBLProxy)); + XToken(address(xSTBLProxy)) + .initialize(platform, mainToken, address(xStakingProxy), revenueRouter, "xStability", "xSTBL"); + + // ---------------------- Write results + vm.stopBroadcast(); + + configDeployed.set("xToken", address(xSTBLProxy)); + configDeployed.set("xStaking", address(xStakingProxy)); + } + + function testDeployScript() external {} +} diff --git a/script/deploy-tokenomics/xTokenBridge.s.sol b/script/deploy-tokenomics/xTokenBridge.s.sol new file mode 100644 index 00000000..2bed3ecc --- /dev/null +++ b/script/deploy-tokenomics/xTokenBridge.s.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import {StdConfig} from "forge-std/StdConfig.sol"; +import {Script} from "forge-std/Script.sol"; +import {Proxy} from "../../src/core/proxy/Proxy.sol"; +import {XTokenBridge} from "../../src/tokenomics/XTokenBridge.sol"; + +contract DeployXTokenBridge is Script { + function run() external { + uint deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + + // ---------------------- Initialize + StdConfig config = new StdConfig("./config.toml", false); // read only config + StdConfig configDeployed = new StdConfig("./config.d.toml", true); // auto-write deployed addresses + + require( + uint(configDeployed.get("OAPP_MAIN_TOKEN").ty.kind) != 0, "OAPP_MAIN_TOKEN is not deployed on the chain" + ); + address bridge = configDeployed.get("OAPP_MAIN_TOKEN").toAddress(); + + require(uint(configDeployed.get("xToken").ty.kind) != 0, "xToken is not deployed on the chain"); + address xToken = configDeployed.get("xToken").toAddress(); + + require(uint(config.get("PLATFORM").ty.kind) != 0, "platform is not set"); + address platform = config.get("PLATFORM").toAddress(); + + require(uint(config.get("LAYER_ZERO_V2_ENDPOINT").ty.kind) != 0, "endpoint is not set"); + address endpoint = config.get("LAYER_ZERO_V2_ENDPOINT").toAddress(); + + require(uint(configDeployed.get("XTokenBridge").ty.kind) == 0, "XTokenBridge already deployed"); + + // ---------------------- Deploy + vm.startBroadcast(deployerPrivateKey); + + Proxy proxy = new Proxy(); + { + address implementation = address(new XTokenBridge(endpoint)); + proxy.initProxy(implementation); + require(proxy.implementation() == implementation, "XTokenBridge: implementation mismatch"); + } + + XTokenBridge(address(proxy)).initialize(platform, bridge, address(xToken)); + + // ---------------------- Write results + vm.stopBroadcast(); + + configDeployed.set("XTokenBridge", address(proxy)); + } + + function testDeployScript() external {} +} diff --git a/script/setup-bridges/Setup.BridgedPriceOracle.Plasma.t.sol b/script/setup-bridges/Setup.BridgedPriceOracle.Plasma.t.sol new file mode 100644 index 00000000..e49e2fcd --- /dev/null +++ b/script/setup-bridges/Setup.BridgedPriceOracle.Plasma.t.sol @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {StdConfig} from "forge-std/StdConfig.sol"; +import {IPlatform} from "../../src/interfaces/IPlatform.sol"; +import {Variable, LibVariable} from "forge-std/LibVariable.sol"; +import {Test} from "forge-std/Test.sol"; +import {BridgeTestLib} from "../../test/tokenomics/libs/BridgeTestLib.sol"; // todo +import {SonicConstantsLib} from "../../chains/sonic/SonicConstantsLib.sol"; +import {PlasmaConstantsLib} from "../../chains/plasma/PlasmaConstantsLib.sol"; + +contract BridgedPriceOracleSetupPlasmaScript is Test { + using LibVariable for Variable; + + uint internal constant SONIC_CHAIN_ID = 146; + uint internal constant PLASMA_CHAIN_ID = 9745; + + /// @dev Minimum block confirmations to wait on Avalanche + uint64 internal constant MIN_BLOCK_CONFIRMATIONS_SEND_TARGET = 15; + + /// @dev Minimum block confirmations required on Avalanche + uint64 internal constant MIN_BLOCK_CONFIRMATIONS_RECEIVE_TARGET = 10; + + uint32 internal constant MAX_MESSAGE_SIZE = 256; + + function run() external { + uint deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + address delegator = vm.envAddress("LZ_DELEGATOR"); + require(delegator != address(0), "delegator is not set"); + + require(block.chainid == PLASMA_CHAIN_ID, "This script is configured for Plasma only"); + + // ---------------------- Initialize + // StdConfig config = new StdConfig("./config.toml", false); + StdConfig configDeployed = new StdConfig("./config.d.toml", false); + + BridgeTestLib.ChainConfig memory plasma = _createConfigPlasma(configDeployed, delegator); + + // ---------------------- Setup + vm.startBroadcast(deployerPrivateKey); + + address[] memory requiredDVNs = new address[](2); + requiredDVNs[0] = PlasmaConstantsLib.PLASMA_DVN_LAYER_ZERO_PUSH; + requiredDVNs[1] = PlasmaConstantsLib.PLASMA_DVN_NETHERMIND_PUSH; + // requiredDVNs[2] = PLASMA_DVN_HORIZON; + + BridgeTestLib._setupOAppOnChain( + plasma, + SonicConstantsLib.LAYER_ZERO_V2_ENDPOINT_ID, + requiredDVNs, + MIN_BLOCK_CONFIRMATIONS_SEND_TARGET, + MAX_MESSAGE_SIZE, + MIN_BLOCK_CONFIRMATIONS_RECEIVE_TARGET + ); + + vm.stopBroadcast(); + } + + function testDeployScript() external {} + + function _createConfigPlasma( + StdConfig configDeployed, + address delegator_ + ) internal view returns (BridgeTestLib.ChainConfig memory) { + require( + uint(configDeployed.get(PLASMA_CHAIN_ID, "BRIDGED_PRICE_ORACLE_MAIN_TOKEN").ty.kind) != 0, + "Price aggregator is not deployed on Plasma" + ); + address oapp = configDeployed.get(PLASMA_CHAIN_ID, "BRIDGED_PRICE_ORACLE_MAIN_TOKEN").toAddress(); + + // we don't use following data in thi script + // require(uint(configDeployed.get(PLASMA_CHAIN_ID, "xToken").ty.kind) != 0, "xToken is not deployed on Plasma"); + // address xToken = configDeployed.get(PLASMA_CHAIN_ID, "xToken").toAddress(); + // + // require(uint(configDeployed.get(PLASMA_CHAIN_ID, "XTokenBridge").ty.kind) != 0, "XTokenBridge is not deployed on Plasma"); + // address xTokenBridge = configDeployed.get(PLASMA_CHAIN_ID, "XTokenBridge").toAddress(); + + return BridgeTestLib.ChainConfig({ + fork: 0, + multisig: IPlatform(PlasmaConstantsLib.PLATFORM).multisig(), + oapp: oapp, + endpointId: PlasmaConstantsLib.LAYER_ZERO_V2_ENDPOINT_ID, + endpoint: PlasmaConstantsLib.LAYER_ZERO_V2_ENDPOINT, + sendLib: PlasmaConstantsLib.LAYER_ZERO_V2_SEND_ULN_302, + receiveLib: PlasmaConstantsLib.LAYER_ZERO_V2_RECEIVE_ULN_302, + platform: PlasmaConstantsLib.PLATFORM, + executor: PlasmaConstantsLib.LAYER_ZERO_V2_EXECUTOR, + xToken: address(0), // xToken, + xTokenBridge: address(0), // xTokenBridge, + delegator: delegator_ + }); + } +} diff --git a/script/setup-bridges/Setup.BridgedToken.Plasma.t.sol b/script/setup-bridges/Setup.BridgedToken.Plasma.t.sol new file mode 100644 index 00000000..fd48c582 --- /dev/null +++ b/script/setup-bridges/Setup.BridgedToken.Plasma.t.sol @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {StdConfig} from "forge-std/StdConfig.sol"; +import {IPlatform} from "../../src/interfaces/IPlatform.sol"; +import {Variable, LibVariable} from "forge-std/LibVariable.sol"; +import {Test} from "forge-std/Test.sol"; +import {BridgeTestLib} from "../../test/tokenomics/libs/BridgeTestLib.sol"; // todo +import {SonicConstantsLib} from "../../chains/sonic/SonicConstantsLib.sol"; +import {PlasmaConstantsLib} from "../../chains/plasma/PlasmaConstantsLib.sol"; + +contract BridgedTokenSetupPlasmaScript is Test { + using LibVariable for Variable; + + uint internal constant SONIC_CHAIN_ID = 146; + uint internal constant PLASMA_CHAIN_ID = 9745; + + /// @dev Minimum block confirmations to wait on Plasma + uint64 internal constant MIN_BLOCK_CONFIRMATIONS_SEND_TARGET = 15; + + /// @dev Minimum block confirmations required on Plasma + uint64 internal constant MIN_BLOCK_CONFIRMATIONS_RECEIVE_TARGET = 10; + + uint32 internal constant MAX_MESSAGE_SIZE = 256; + + function run() external { + uint deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + address delegator = vm.envAddress("LZ_DELEGATOR"); + require(delegator != address(0), "delegator is not set"); + + require(block.chainid == PLASMA_CHAIN_ID, "This script is configured for Plasma only"); + + // ---------------------- Initialize + // StdConfig config = new StdConfig("./config.toml", false); + StdConfig configDeployed = new StdConfig("./config.d.toml", false); + + BridgeTestLib.ChainConfig memory plasma = _createConfigPlasma(configDeployed, delegator); + + // ---------------------- Setup + vm.startBroadcast(deployerPrivateKey); + + address[] memory requiredDVNs = new address[](2); + requiredDVNs[0] = PlasmaConstantsLib.PLASMA_DVN_LAYER_ZERO_PUSH; + requiredDVNs[1] = PlasmaConstantsLib.PLASMA_DVN_NETHERMIND_PUSH; + // requiredDVNs[2] = PLASMA_DVN_HORIZON; + + BridgeTestLib._setupOAppOnChain( + plasma, + SonicConstantsLib.LAYER_ZERO_V2_ENDPOINT_ID, + requiredDVNs, + MIN_BLOCK_CONFIRMATIONS_SEND_TARGET, + MAX_MESSAGE_SIZE, + MIN_BLOCK_CONFIRMATIONS_RECEIVE_TARGET + ); + + vm.stopBroadcast(); + } + + function testDeployScript() external {} + + function _createConfigPlasma( + StdConfig configDeployed, + address delegator_ + ) internal view returns (BridgeTestLib.ChainConfig memory) { + require( + uint(configDeployed.get(PLASMA_CHAIN_ID, "OAPP_MAIN_TOKEN").ty.kind) != 0, + "Bridged token is not deployed on Plasma" + ); + address oapp = configDeployed.get(PLASMA_CHAIN_ID, "OAPP_MAIN_TOKEN").toAddress(); + + // we don't use following data in thi script + // require(uint(configDeployed.get(PLASMA_CHAIN_ID, "xToken").ty.kind) != 0, "xToken is not deployed on Plasma"); + // address xToken = configDeployed.get(PLASMA_CHAIN_ID, "xToken").toAddress(); + // + // require(uint(configDeployed.get(PLASMA_CHAIN_ID, "XTokenBridge").ty.kind) != 0, "XTokenBridge is not deployed on Plasma"); + // address xTokenBridge = configDeployed.get(PLASMA_CHAIN_ID, "XTokenBridge").toAddress(); + + return BridgeTestLib.ChainConfig({ + fork: 0, + multisig: IPlatform(PlasmaConstantsLib.PLATFORM).multisig(), + oapp: oapp, + endpointId: PlasmaConstantsLib.LAYER_ZERO_V2_ENDPOINT_ID, + endpoint: PlasmaConstantsLib.LAYER_ZERO_V2_ENDPOINT, + sendLib: PlasmaConstantsLib.LAYER_ZERO_V2_SEND_ULN_302, + receiveLib: PlasmaConstantsLib.LAYER_ZERO_V2_RECEIVE_ULN_302, + platform: PlasmaConstantsLib.PLATFORM, + executor: PlasmaConstantsLib.LAYER_ZERO_V2_EXECUTOR, + xToken: address(0), // xToken, + xTokenBridge: address(0), // xTokenBridge, + delegator: delegator_ + }); + } +} diff --git a/script/setup-bridges/Setup.PriceAggregatorOAppPlasma.Sonic.t.sol b/script/setup-bridges/Setup.PriceAggregatorOAppPlasma.Sonic.t.sol new file mode 100644 index 00000000..1fb37890 --- /dev/null +++ b/script/setup-bridges/Setup.PriceAggregatorOAppPlasma.Sonic.t.sol @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {StdConfig} from "forge-std/StdConfig.sol"; +import {IPlatform} from "../../src/interfaces/IPlatform.sol"; +import {Variable, LibVariable} from "forge-std/LibVariable.sol"; +import {Test} from "forge-std/Test.sol"; +import {SonicConstantsLib} from "../../chains/sonic/SonicConstantsLib.sol"; +import {PlasmaConstantsLib} from "../../chains/plasma/PlasmaConstantsLib.sol"; +import {BridgeTestLib} from "../../test/tokenomics/libs/BridgeTestLib.sol"; // todo + +contract PriceAggregatorOAppPlasmaSetupSonicScript is Test { + using LibVariable for Variable; + + uint internal constant SONIC_CHAIN_ID = 146; + uint internal constant PLASMA_CHAIN_ID = 9745; + + /// @dev Minimum block confirmations to wait on Sonic + uint64 internal constant MIN_BLOCK_CONFIRMATIONS_SEND_SONIC = 15; + + /// @dev Minimum block confirmations required on Sonic + uint64 internal constant MIN_BLOCK_CONFIRMATIONS_RECEIVE_SONIC = 10; + + uint32 internal constant MAX_MESSAGE_SIZE = 256; + + function run() external { + uint deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + address delegator = vm.envAddress("LZ_DELEGATOR"); + require(delegator != address(0), "delegator is not set"); + + require(block.chainid == SONIC_CHAIN_ID, "PriceAggregatorOApp is deployed on Sonic only"); + + // ---------------------- Initialize + // StdConfig config = new StdConfig("./config.toml", false); + StdConfig configDeployed = new StdConfig("./config.d.toml", false); + + BridgeTestLib.ChainConfig memory sonic = _createConfigSonic(configDeployed, delegator); + + // ---------------------- Setup + vm.startBroadcast(deployerPrivateKey); + + address[] memory requiredDVNs = new address[](2); + requiredDVNs[0] = SonicConstantsLib.SONIC_DVN_LAYER_ZERO_PUSH; + requiredDVNs[1] = SonicConstantsLib.SONIC_DVN_HORIZEN_PUSH; + + BridgeTestLib._setupOAppOnChain( + sonic, + PlasmaConstantsLib.LAYER_ZERO_V2_ENDPOINT_ID, + requiredDVNs, + MIN_BLOCK_CONFIRMATIONS_SEND_SONIC, + MAX_MESSAGE_SIZE, + MIN_BLOCK_CONFIRMATIONS_RECEIVE_SONIC + ); + + vm.stopBroadcast(); + } + + function testDeployScript() external {} + + function _createConfigSonic( + StdConfig configDeployed, + address delegator_ + ) internal view returns (BridgeTestLib.ChainConfig memory) { + require( + uint(configDeployed.get(SONIC_CHAIN_ID, "PRICE_AGGREGATOR_OAPP_MAIN_TOKEN").ty.kind) != 0, + "Price aggregator is not deployed on Sonic" + ); + address oapp = configDeployed.get(SONIC_CHAIN_ID, "PRICE_AGGREGATOR_OAPP_MAIN_TOKEN").toAddress(); + + require(uint(configDeployed.get(SONIC_CHAIN_ID, "xToken").ty.kind) != 0, "xToken is not deployed on Sonic"); + address xToken = configDeployed.get(SONIC_CHAIN_ID, "xToken").toAddress(); + + // require(uint(configDeployed.get(SONIC_CHAIN_ID, "XTokenBridge").ty.kind) != 0, "XTokenBridge is not deployed on Sonic"); + // address xTokenBridge = configDeployed.get(SONIC_CHAIN_ID, "XTokenBridge").toAddress(); + + return BridgeTestLib.ChainConfig({ + fork: 0, + multisig: IPlatform(SonicConstantsLib.PLATFORM).multisig(), + oapp: oapp, + endpointId: SonicConstantsLib.LAYER_ZERO_V2_ENDPOINT_ID, + endpoint: SonicConstantsLib.LAYER_ZERO_V2_ENDPOINT, + sendLib: SonicConstantsLib.LAYER_ZERO_V2_SEND_ULN_302, + receiveLib: SonicConstantsLib.LAYER_ZERO_V2_RECEIVE_ULN_302, + platform: SonicConstantsLib.PLATFORM, + executor: SonicConstantsLib.LAYER_ZERO_V2_EXECUTOR, + xToken: xToken, + xTokenBridge: address(0), // xTokenBridge, // not required here + delegator: delegator_ + }); + } +} diff --git a/script/setup-bridges/Setup.TokenOFTAdapter.Sonic.t.sol b/script/setup-bridges/Setup.TokenOFTAdapter.Sonic.t.sol new file mode 100644 index 00000000..a2d90329 --- /dev/null +++ b/script/setup-bridges/Setup.TokenOFTAdapter.Sonic.t.sol @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {StdConfig} from "forge-std/StdConfig.sol"; +import {IPlatform} from "../../src/interfaces/IPlatform.sol"; +import {Variable, LibVariable} from "forge-std/LibVariable.sol"; +import {Test} from "forge-std/Test.sol"; +import {SonicConstantsLib} from "../../chains/sonic/SonicConstantsLib.sol"; +import {PlasmaConstantsLib} from "../../chains/plasma/PlasmaConstantsLib.sol"; +import {BridgeTestLib} from "../../test/tokenomics/libs/BridgeTestLib.sol"; // todo + +contract TokenOFTAdapterSonicSetupScript is Test { + using LibVariable for Variable; + + uint internal constant SONIC_CHAIN_ID = 146; + uint internal constant PLASMA_CHAIN_ID = 9745; + + /// @dev Minimum block confirmations to wait on Sonic + uint64 internal constant MIN_BLOCK_CONFIRMATIONS_SEND_SONIC = 15; + + /// @dev Minimum block confirmations required on Sonic + uint64 internal constant MIN_BLOCK_CONFIRMATIONS_RECEIVE_SONIC = 10; + + uint32 internal constant MAX_MESSAGE_SIZE = 256; + + function run() external { + uint deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + address delegator = vm.envAddress("LZ_DELEGATOR"); + require(delegator != address(0), "delegator is not set"); + + require(block.chainid == SONIC_CHAIN_ID, "TokenOFTAdapter is deployed on Sonic only"); + + // ---------------------- Initialize + // StdConfig config = new StdConfig("./config.toml", false); + StdConfig configDeployed = new StdConfig("./config.d.toml", false); + + BridgeTestLib.ChainConfig memory sonic = _createConfigSonic(configDeployed, delegator); + + // ---------------------- Setup + vm.startBroadcast(deployerPrivateKey); + + address[] memory requiredDVNs = new address[](2); + requiredDVNs[0] = SonicConstantsLib.SONIC_DVN_LAYER_ZERO_PUSH; + requiredDVNs[1] = SonicConstantsLib.SONIC_DVN_HORIZEN_PUSH; + + BridgeTestLib._setupOAppOnChain( + sonic, + PlasmaConstantsLib.LAYER_ZERO_V2_ENDPOINT_ID, + requiredDVNs, + MIN_BLOCK_CONFIRMATIONS_SEND_SONIC, + MAX_MESSAGE_SIZE, + MIN_BLOCK_CONFIRMATIONS_RECEIVE_SONIC + ); + + vm.stopBroadcast(); + } + + function testDeployScript() external {} + + function _createConfigSonic( + StdConfig configDeployed, + address delegator_ + ) internal view returns (BridgeTestLib.ChainConfig memory) { + require( + uint(configDeployed.get(SONIC_CHAIN_ID, "OAPP_MAIN_TOKEN").ty.kind) != 0, + "TokenOFTAdapter is not deployed on Sonic" + ); + address oapp = configDeployed.get(SONIC_CHAIN_ID, "OAPP_MAIN_TOKEN").toAddress(); + + require(uint(configDeployed.get(SONIC_CHAIN_ID, "xToken").ty.kind) != 0, "xToken is not deployed on Sonic"); + address xToken = configDeployed.get(SONIC_CHAIN_ID, "xToken").toAddress(); + + // require(uint(configDeployed.get(SONIC_CHAIN_ID, "XTokenBridge").ty.kind) != 0, "XTokenBridge is not deployed on Sonic"); + // address xTokenBridge = configDeployed.get(SONIC_CHAIN_ID, "XTokenBridge").toAddress(); + + return BridgeTestLib.ChainConfig({ + fork: 0, + multisig: IPlatform(SonicConstantsLib.PLATFORM).multisig(), + oapp: oapp, + endpointId: SonicConstantsLib.LAYER_ZERO_V2_ENDPOINT_ID, + endpoint: SonicConstantsLib.LAYER_ZERO_V2_ENDPOINT, + sendLib: SonicConstantsLib.LAYER_ZERO_V2_SEND_ULN_302, + receiveLib: SonicConstantsLib.LAYER_ZERO_V2_RECEIVE_ULN_302, + platform: SonicConstantsLib.PLATFORM, + executor: SonicConstantsLib.LAYER_ZERO_V2_EXECUTOR, + xToken: xToken, + xTokenBridge: address(0), // xTokenBridge, // not required here + delegator: delegator_ + }); + } +} diff --git a/script/upgrade-core/PrepareUpgrade.25.10.3-alpha.s.sol b/script/upgrade-core/PrepareUpgrade.25.10.3-alpha.s.sol index cbcb0f20..9f698693 100644 --- a/script/upgrade-core/PrepareUpgrade.25.10.3-alpha.s.sol +++ b/script/upgrade-core/PrepareUpgrade.25.10.3-alpha.s.sol @@ -5,9 +5,9 @@ import {Script} from "forge-std/Script.sol"; import {Platform} from "../../src/core/Platform.sol"; import {Proxy} from "../../src/core/proxy/Proxy.sol"; import {XStaking} from "../../src/tokenomics/XStaking.sol"; -import {XSTBL} from "../../src/tokenomics/XSTBL.sol"; -import {StabilityDAO} from "../../src/tokenomics/StabilityDAO.sol"; -import {IStabilityDAO} from "../../src/interfaces/IStabilityDAO.sol"; +import {XToken} from "../../src/tokenomics/XToken.sol"; +import {DAO} from "../../src/tokenomics/DAO.sol"; +import {IDAO} from "../../src/interfaces/IDAO.sol"; import {SonicConstantsLib} from "../../chains/sonic/SonicConstantsLib.sol"; import {RevenueRouter} from "../../src/tokenomics/RevenueRouter.sol"; @@ -22,7 +22,7 @@ contract PrepareUpgrade25103alpha is Script { new XStaking(); // XSTBL 1.1.0 - new XSTBL(); + new XToken(); // Platform 1.6.2: IPlatform.stabilityDAO() new Platform(); @@ -32,19 +32,21 @@ contract PrepareUpgrade25103alpha is Script { // StabilityDAO Proxy proxy = new Proxy(); - proxy.initProxy(address(new StabilityDAO())); - StabilityDAO(address(proxy)) + proxy.initProxy(address(new DAO())); + DAO(address(proxy)) .initialize( PLATFORM, SonicConstantsLib.TOKEN_XSTBL, SonicConstantsLib.XSTBL_XSTAKING, - IStabilityDAO.DaoParams({ + IDAO.DaoParams({ minimalPower: 4000e18, exitPenalty: 50_00, // 50%, decimals 1e4 proposalThreshold: 10_000, // 10% quorum: 30_000, // 30% powerAllocationDelay: 1 days - }) + }), + "Stability DAO", + "STBL_DAO" ); vm.stopBroadcast(); } diff --git a/script/upgrade-core/PrepareUpgrade.25.10.5-alpha.s.sol b/script/upgrade-core/PrepareUpgrade.25.10.5-alpha.s.sol index d0350c67..11a895e7 100644 --- a/script/upgrade-core/PrepareUpgrade.25.10.5-alpha.s.sol +++ b/script/upgrade-core/PrepareUpgrade.25.10.5-alpha.s.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.28; import {SonicConstantsLib} from "../../chains/sonic/SonicConstantsLib.sol"; import {Script} from "forge-std/Script.sol"; -import {StabilityDAO} from "../../src/tokenomics/StabilityDAO.sol"; +import {DAO} from "../../src/tokenomics/DAO.sol"; contract PrepareUpgrade25105alpha is Script { address public constant PLATFORM = SonicConstantsLib.PLATFORM; @@ -13,7 +13,7 @@ contract PrepareUpgrade25105alpha is Script { vm.startBroadcast(deployerPrivateKey); // StabilityDAO 1.0.1 - new StabilityDAO(); + new DAO(); vm.stopBroadcast(); } diff --git a/script/upgrade-core/PrepareUpgrade.25.11.0-alpha.s.sol b/script/upgrade-core/PrepareUpgrade.25.11.0-alpha.s.sol new file mode 100755 index 00000000..d2bb03d6 --- /dev/null +++ b/script/upgrade-core/PrepareUpgrade.25.11.0-alpha.s.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import {SonicConstantsLib} from "../../chains/sonic/SonicConstantsLib.sol"; +import {Script} from "forge-std/Script.sol"; +import {Recovery} from "../../src/tokenomics/Recovery.sol"; +import {Platform} from "../../src/core/Platform.sol"; + +contract PrepareUpgrade25110alpha is Script { + address public constant PLATFORM = SonicConstantsLib.PLATFORM; + + function run() external { + uint deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + vm.startBroadcast(deployerPrivateKey); + + // Recovery 1.2.2 + new Recovery(); + + // Platform 1.6.4 + new Platform(); + + vm.stopBroadcast(); + } + + function testPrepareUpgrade() external {} +} diff --git a/script/upgrade-core/PrepareUpgrade.25.12.0-alpha.s.sol b/script/upgrade-core/PrepareUpgrade.25.12.0-alpha.s.sol new file mode 100755 index 00000000..57cc5168 --- /dev/null +++ b/script/upgrade-core/PrepareUpgrade.25.12.0-alpha.s.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import {SonicConstantsLib} from "../../chains/sonic/SonicConstantsLib.sol"; +import {Script} from "forge-std/Script.sol"; +import {XStaking} from "../../src/tokenomics/XStaking.sol"; +import {DAO} from "../../src/tokenomics/DAO.sol"; +import {XToken} from "../../src/tokenomics/XToken.sol"; +import {RevenueRouter} from "../../src/tokenomics/RevenueRouter.sol"; + +contract PrepareUpgrade25120alpha is Script { + address public constant PLATFORM = SonicConstantsLib.PLATFORM; + + function run() external { + uint deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + vm.startBroadcast(deployerPrivateKey); + + // XStaking 1.1.2 + new XStaking(); + + // DAO 1.1.0 + new DAO(); + + // XToken 1.2.0 + new XToken(); + + // RevenueRouter 1.8.0 + new RevenueRouter(); + + vm.stopBroadcast(); + } + + function testPrepareUpgrade() external {} +} diff --git a/script/upgrade-core/PrepareUpgrade.25.12.1-alpha.s.sol b/script/upgrade-core/PrepareUpgrade.25.12.1-alpha.s.sol new file mode 100755 index 00000000..19c57fb3 --- /dev/null +++ b/script/upgrade-core/PrepareUpgrade.25.12.1-alpha.s.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import {SonicConstantsLib} from "../../chains/sonic/SonicConstantsLib.sol"; +import {Script} from "forge-std/Script.sol"; +import {Platform} from "../../src/core/Platform.sol"; +import {XTokenBridge} from "../../src/tokenomics/XTokenBridge.sol"; +import {TokenOFTAdapter} from "../../src/tokenomics/TokenOFTAdapter.sol"; +import {BridgedToken} from "../../src/tokenomics/BridgedToken.sol"; +import {PlasmaConstantsLib} from "../../chains/plasma/PlasmaConstantsLib.sol"; + +contract PrepareUpgrade25121alpha is Script { + uint internal constant SONIC_CHAIN_ID = 146; + uint internal constant PLASMA_CHAIN_ID = 9745; + + function run() external { + uint deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + vm.startBroadcast(deployerPrivateKey); + + if (block.chainid == SONIC_CHAIN_ID) { + /// XTokenBridge 1.0.1 + new XTokenBridge(SonicConstantsLib.LAYER_ZERO_V2_ENDPOINT); + + /// TokenOFTAdapter 1.0.1 + new TokenOFTAdapter(SonicConstantsLib.TOKEN_STBL, SonicConstantsLib.LAYER_ZERO_V2_ENDPOINT); + } else if (block.chainid == PLASMA_CHAIN_ID) { + /// Platform 1.6.4 + new Platform(); + + /// XTokenBridge 1.0.1 + new XTokenBridge(PlasmaConstantsLib.LAYER_ZERO_V2_ENDPOINT); + + /// BridgedToken 1.0.2 + new BridgedToken(PlasmaConstantsLib.LAYER_ZERO_V2_ENDPOINT); + } + + vm.stopBroadcast(); + } + + function testPrepareUpgrade() external {} +} diff --git a/src/interfaces/IBridgedPriceOracle.sol b/src/interfaces/IBridgedPriceOracle.sol new file mode 100644 index 00000000..39bf8b8d --- /dev/null +++ b/src/interfaces/IBridgedPriceOracle.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.22; + +import {IAggregatorInterfaceMinimal} from "../integrations/chainlink/IAggregatorInterfaceMinimal.sol"; + +interface IBridgedPriceOracle is IAggregatorInterfaceMinimal { + error InvalidMessageFormat(); + + /// @notice Emitted when price is updated + event PriceUpdated(uint priceUsd18, uint priceTimestamp); + event PriceUpdateSkipped(uint priceUsd18, uint priceTimestamp); + + /// @notice Returns the latest price in USD with 18 decimals + /// @return price Price in USD with 18 decimals + /// @return priceTimestamp Timestamp of the price - moment of price update in source PriceAggregator + function getPriceUsd18() external view returns (uint price, uint priceTimestamp); + + /// @notice Token for which this oracle provides price + function tokenSymbol() external view returns (string memory); + + /// @notice Initialize with platform and token symbol + /// @param delegate_ The delegate capable of making OApp configurations inside of the endpoint. + /// Pass 0 to set multisig as the delegate. Owner (multisig) is able to change it using setDelegate. + function initialize(address platform_, string memory tokenSymbol_, address delegate_) external; +} diff --git a/src/interfaces/IBridgedToken.sol b/src/interfaces/IBridgedToken.sol new file mode 100644 index 00000000..981d4fd7 --- /dev/null +++ b/src/interfaces/IBridgedToken.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {IOFTPausable} from "./IOFTPausable.sol"; + +interface IBridgedToken is IOFTPausable { + event BridgedTokenName(string newName); + event BridgedTokenSymbol(string newSymbol); + + /// @param delegate_ The delegate capable of making OApp configurations inside of the endpoint. + /// Pass 0 to set multisig as the delegate. Owner (multisig) is able to change it using setDelegate. + function initialize(address platform_, string memory name_, string memory symbol_, address delegate_) external; + + /// @notice Sets a new name for the token. + function setName(string calldata newName) external; + + /// @notice Sets a new symbol for the token. + function setSymbol(string calldata newSymbol) external; +} diff --git a/src/interfaces/IDAO.sol b/src/interfaces/IDAO.sol new file mode 100644 index 00000000..bfdb4405 --- /dev/null +++ b/src/interfaces/IDAO.sol @@ -0,0 +1,140 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; + +interface IDAO is IERC20, IERC20Metadata { + /// @notice Parameters of Stability DAO + /// @dev For details see https://stabilitydao.gitbook.io/stability/stability-dao/governance#current-parameters + struct DaoParams { + /// @notice Minimal amount of xToken tokens required to have DAO-token tokens, decimals 18 + uint minimalPower; + /// @notice xToken instant exit penalty, decimals 1e4, i.e. 50_00 = 50% + /// Set 0 to use default value xToken.DEFAULT_SLASHING_PENALTY + uint exitPenalty; + /// @notice Min percent of power that a user should have to be able to create new proposal. Decimals 1e5, i.e. 50_000 = 50% + uint proposalThreshold; + /// @notice A percent of votes required to reach quorum for a proposal. Decimals 1e5, i.e. 20_000 = 20% + /// If the total number of votes is less than this percent, proposal is rejected + uint quorum; + /// @notice Inter-chain power allocation delay, i.e. 1 day + uint powerAllocationDelay; + } + + error NonTransferable(); + error NotDelegatedTo(); + error AlreadyDelegated(); + error WrongValue(); + error NotOtherChainsPowersWhitelisted(); + error DelegationForbiddenOnTheChain(); + + event ConfigUpdated(DaoParams newConfig); + event DelegatePower(address from, address to); + event UnDelegatePower(address from, address to); + event WhitelistOtherChainsPowers(address user, bool whitelisted); + event PowersOtherChainsUpdated(uint timestamp); + event SetDelegationForbiddenOnTheChain(bool forbidden); + event DaoName(string newName); + event DaoSymbol(string newSymbol); + + //region --------------------------------------- Read functions + /// @notice Current DAO config + function config() external view returns (DaoParams memory); + + /// @notice Address of xToken token + function xToken() external view returns (address); + + /// @notice Address of xStaking contract + function xStaking() external view returns (address); + + /// @notice Minimal amount of xToken tokens required to have DAO-token tokens, decimals 18 + function minimalPower() external view returns (uint); + + /// @notice xToken instant exit penalty (slashing penalty), decimals 1e4, i.e. 50_00 = 50% + function exitPenalty() external view returns (uint); + + /// @notice Min percent of power that a user should have to be able to create a new proposal, decimals 1e5, i.e. 50_000 = 50% + function proposalThreshold() external view returns (uint); + + /// @notice A percent of votes required to reach quorum for a proposal, decimals 1e5, i.e. 20_000 = 20% + /// If the total number of votes is less than this percent, proposal is rejected + function quorum() external view returns (uint); + + /// @notice Inter-chain power allocation delay, i.e. 1 day + function powerAllocationDelay() external view returns (uint); + + /// @notice Get total power of a user. + /// The power = user's own (not-delegated) balance of DAO-token + balances of all users that delegated to him + /// If user has balance of staked xToken below minimalPower, his power is 0 + function getVotes(address user_) external view returns (uint); + + /// @notice Get current power values for the given user. + /// @param user_ The address of the user. + /// @return localPower Power on the current chain. This power can be delegated to other user (delegates.delegatedTo}. + /// @return otherPower Power on other chains. This power can be delegated to other user (delegates.delegatedTo}. + function getPowers(address user_) external view returns (uint localPower, uint otherPower); + + /// @notice Get delegation info of a user + /// @return delegatedTo The address to whom the user has delegated his voting power (or address(0) if not delegated) + /// @return delegators The list of addresses that have delegated their voting power to the user + function delegates(address user_) external view returns (address delegatedTo, address[] memory delegators); + + /// @notice Get list of users and their total powers on the other (not current) chains + /// @return timestamp The time when the powers were last updated through {setOtherChainsPowers} + /// @return users The list of user addresses + /// @return powers The list of total powers corresponding to the users list + function getOtherChainsPowers() external view returns (uint timestamp, address[] memory users, uint[] memory powers); + + /// @notice Check if a user is whitelisted to call {setOtherChainsPowers} + function isWhitelistedForOtherChainsPowers(address user_) external view returns (bool); + + /// @notice True if delegation of voting power is forbidden + function delegationForbidden() external view returns (bool); + //endregion --------------------------------------- Read functions + + //region --------------------------------------- Write functions + /// @dev Init + function initialize( + address platform_, + address xToken_, + address xStaking_, + DaoParams memory config_, + string memory name_, + string memory symbol_ + ) external; + + /// @notice Update DAO config + /// XStaking.syncDAOBalances() must be called after changing of minimalPower value + /// @custom:restricted To multisig or governance + function updateConfig(DaoParams memory p) external; + + /// @custom:restricted To xStaking + function mint(address account, uint amount) external; + + /// @custom:restricted To xStaking + function burn(address account, uint amount) external; + + /// @notice Delegate all voting power to another user. + /// To remove delegation just delegate the power to yourself or to address(0). + /// @custom:restricted Anyone can call this function + function setPowerDelegation(address to) external; + + /// @notice Set whitelist status for a user to call {setOtherChainsPowers} + function setWhitelistedForOtherChainsPowers(address user, bool whitelisted) external; + + /// @notice Set list of users and their total powers on the other (not current) chains + /// @custom:restricted whitelist {whitelistOtherChainsPowers} + function updateOtherChainsPowers(address[] memory users, uint[] memory powers) external; + + /// @notice Forbid or allow delegation of voting power + function setDelegationForbidden(bool forbidden) external; + + /// @notice Sets a new name for the token. + function setName(string calldata newName) external; + + /// @notice Sets a new symbol for the token. + function setSymbol(string calldata newSymbol) external; + + //endregion --------------------------------------- Write functions +} diff --git a/src/interfaces/IOFTPausable.sol b/src/interfaces/IOFTPausable.sol new file mode 100644 index 00000000..67d1d41a --- /dev/null +++ b/src/interfaces/IOFTPausable.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {IOFT} from "@layerzerolabs/oft-evm/contracts/interfaces/IOFT.sol"; + +interface IOFTPausable is IOFT { + error Paused(); + event Pause(address indexed account, bool paused); + + /// @notice True if the given account is paused and is not able to transfer bridget tokens + function paused(address account_) external view returns (bool); + + /// @notice Set paused state for account + /// @param account Address of account + /// @param paused_ True - set paused, false - unpaused + function setPaused(address account, bool paused_) external; + + /// @dev See OptionsBuilder.addExecutorLzReceiveOption + /// @param gas_ The gasLimit used on the lzReceive() function in the OApp. + /// @param value_ The msg.value passed to the lzReceive() function in the OApp (use 0). + function buildOptions(uint128 gas_, uint128 value_) external pure returns (bytes memory); +} diff --git a/src/interfaces/IPriceAggregatorOApp.sol b/src/interfaces/IPriceAggregatorOApp.sol new file mode 100644 index 00000000..f6b08deb --- /dev/null +++ b/src/interfaces/IPriceAggregatorOApp.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.22; + +import {MessagingFee} from "@layerzerolabs/oapp-evm-upgradeable/contracts/oapp/OAppUpgradeable.sol"; + +interface IPriceAggregatorOApp { + error NotWhitelisted(); + error UnsupportedOperation(); + + event PriceUpdated(uint destEid, uint priceUsd18, uint priceTimestamp); + event ChangeWhitelist(address caller, bool whitelisted); + event SendPriceMessage(uint destEid, uint priceUsd18, uint priceTimestamp); + + /// @param entity_ The entity (vault or asset) to get price for from PriceAggregator + /// @param delegate_ The delegate capable of making OApp configurations inside of the endpoint. + /// Pass 0 to set multisig as the delegate. Owner (multisig) is able to change it using setDelegate. + function initialize(address platform_, address entity_, address delegate_) external; + + /// @notice Address of the entity (vault or asset) to get price for + function entity() external view returns (address); + + /// @notice True if the given caller is whitelisted to request price updates + function isWhitelisted(address caller) external view returns (bool); + + /// @notice Change whitelist status for the given caller + /// @param caller Address of the caller + /// @param whitelisted True to add to whitelist, false to remove from whitelist + function changeWhitelist(address caller, bool whitelisted) external; + + /// @notice Quote the gas needed to pay for sending price message to the given destination chain endpoint ID. + /// The message is generated internally as a packet of price value and timestamp taken from the price aggregator + /// @param dstEid_ Destination chain endpoint ID, see https://docs.layerzero.network/v2/concepts/glossary#endpoint-id + /// @param options_ Additional options for the message. Use OptionsBuilder.addExecutorLzReceiveOption() + /// @param payInLzToken_ Whether to return fee in ZRO token. + /// @return fee A `MessagingFee` struct containing the calculated gas fee in either the native token or ZRO token. + function quotePriceMessage( + uint32 dstEid_, + bytes memory options_, + bool payInLzToken_ + ) external view returns (MessagingFee memory fee); + + /// @notice Send price message to a remote BridgedPriceOracle on another chain. + /// The message is generated internally as a packet of price value and timestamp taken from the price aggregator + /// @param dstEid_ Destination chain endpoint ID, see https://docs.layerzero.network/v2/concepts/glossary#endpoint-id + /// @param options_ Additional options for the message. Use OptionsBuilder.addExecutorLzReceiveOption() + /// @param fee_ A `MessagingFee` struct containing the gas fee to be paid + function sendPriceMessage(uint32 dstEid_, bytes memory options_, MessagingFee memory fee_) external payable; +} diff --git a/src/interfaces/IRecovery.sol b/src/interfaces/IRecovery.sol index e88f4956..943fc40a 100644 --- a/src/interfaces/IRecovery.sol +++ b/src/interfaces/IRecovery.sol @@ -1,10 +1,9 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.28; -interface IRecovery { - /// @dev Init - function initialize(address platform_) external; +import {IRecoveryBase} from "./IRecoveryBase.sol"; +interface IRecovery is IRecoveryBase { /// @notice Returns the list of registered recovery pools function recoveryPools() external view returns (address[] memory); @@ -42,10 +41,6 @@ interface IRecovery { /// @notice Set receiver of recovedry tokens, 0 - the tokens should be burnt function setReceiver(address recoveryToken_, address receiver_) external; - /// @notice Revenue Router calls this function to notify that some tokens were transferred to this contract - /// @param tokens Addresses of the tokens that were transferred - function registerAssets(address[] memory tokens) external; - /// @notice Swap registered tokens to meta vault tokens. The meta vault token is selected from the given recovery pool. /// @param tokens Addresses of registered tokens to be swapped. They should be asked through {getListTokensToSwap} /// Number of tokens should be limited to avoid gas limit excess, so this function probably should be called several times diff --git a/src/interfaces/IRecoveryBase.sol b/src/interfaces/IRecoveryBase.sol new file mode 100644 index 00000000..079c9de3 --- /dev/null +++ b/src/interfaces/IRecoveryBase.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +interface IRecoveryBase { + /// @dev Init + function initialize(address platform_) external; + + /// @notice Revenue Router calls this function to notify that some tokens were transferred to this contract + /// @param tokens Addresses of the tokens that were transferred + function registerAssets(address[] memory tokens) external; +} diff --git a/src/interfaces/IRecoveryRelayer.sol b/src/interfaces/IRecoveryRelayer.sol new file mode 100644 index 00000000..f68d92f3 --- /dev/null +++ b/src/interfaces/IRecoveryRelayer.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import {IRecoveryBase} from "./IRecoveryBase.sol"; + +interface IRecoveryRelayer is IRecoveryBase { + /// @notice Returns the threshold amount for the given token + function threshold(address token) external view returns (uint); + + /// @notice Returns true if the operator is whitelisted + /// Multisig is always whitelisted. + function whitelisted(address operator_) external view returns (bool); + + /// @notice Returns true if the token is registered by {registerAssets} + function isTokenRegistered(address token) external view returns (bool); + + /// @notice Return list of registered tokens with amounts exceeding thresholds + function getListTokensToSwap() external view returns (address[] memory tokens); + + /// @notice Return full list of registered tokens + function getListRegisteredTokens() external view returns (address[] memory tokens); + + /// @notice Set threshold amounts for the given tokens + function setThresholds(address[] memory tokens, uint[] memory thresholds) external; + + /// @notice Add or remove operator from the whitelist + function changeWhitelist(address operator_, bool add_) external; + + // todo function swapAssets(address[] memory tokens) external; +} diff --git a/src/interfaces/IRevenueRouter.sol b/src/interfaces/IRevenueRouter.sol index 19c6746d..cadb0e4b 100644 --- a/src/interfaces/IRevenueRouter.sol +++ b/src/interfaces/IRevenueRouter.sol @@ -13,6 +13,8 @@ interface IRevenueRouter { event UpdatedUnit(uint unitIndex, UnitType unitType, string name, address feeTreasury); event UnitEpochRevenue(uint periodEnded, string unitName, uint stblRevenue); event ProcessUnitRevenue(uint unitIndex, uint stblGot); + event SetAddresses(address[] addresses); + event SetXShare(uint newShare); /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* CUSTOM ERRORS */ @@ -28,8 +30,8 @@ interface IRevenueRouter { /// @custom:storage-location erc7201:stability.RevenueRouter struct RevenueRouterStorage { - address stbl; - address xStbl; + address token; + address xToken; address xStaking; address feeTreasury; uint xShare; @@ -78,6 +80,9 @@ interface IRevenueRouter { /// @notice Change revenue share for Vaults Unit function setXShare(uint newShare) external; + /// @notice Set addresses of main-token, xToken, xStaking and feeTreasure token. + function setAddresses(address[] memory addresses_) external; + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* USER ACTIONS */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ @@ -92,13 +97,13 @@ interface IRevenueRouter { /// @notice Process platform fee in form of an vault shares function processFeeVault(address vault, uint amount) external; - /// @notice Claim unit fees and swap to STBL + /// @notice Claim unit fees and swap to main-token function processUnitRevenue(uint unitIndex) external; - /// @notice Claim units fees and swap to STBL + /// @notice Claim units fees and swap to main-token (STBL) function processUnitsRevenue() external; - /// @notice Withdraw assets from accumulated vaults + /// @notice Withdraw assets from accumulated vaults (STBL) function processAccumulatedVaults(uint maxVaultsForWithdraw) external; /// @notice Withdraw assets from accumulated vaults @@ -121,10 +126,10 @@ interface IRevenueRouter { /// @notice Current active period function activePeriod() external view returns (uint); - /// @notice Accumulated STBL amount for next distribution by core unit (vault fees) + /// @notice Accumulated main-token amount for next distribution by core unit (vault fees) function pendingRevenue() external view returns (uint); - /// @notice Accumulated STBL amount for next distribution by unit + /// @notice Accumulated main-token amount for next distribution by unit function pendingRevenue(uint unitIndex) external view returns (uint); /// @notice Get Aave pool list to mintToTreasury calls @@ -133,9 +138,12 @@ interface IRevenueRouter { /// @notice Get vault addresses that contract hold on balance, but not withdrew yet function vaultsAccumulated() external view returns (address[] memory); - /// @notice Addresses of STBL, xSTBL, xStaking and feeTreasure token + /// @notice Addresses of main-token, xToken, xStaking and feeTreasure token function addresses() external view returns (address[] memory); /// @notice Get assets that contract hold on balance function assetsAccumulated() external view returns (address[] memory); + + /// @notice Get current xToken revenue share for Vaults Unit + function xShare() external view returns (uint); } diff --git a/src/interfaces/IStabilityDAO.sol b/src/interfaces/IStabilityDAO.sol deleted file mode 100644 index b8d4adee..00000000 --- a/src/interfaces/IStabilityDAO.sol +++ /dev/null @@ -1,84 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.28; - -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; - -interface IStabilityDAO is IERC20, IERC20Metadata { - /// @notice Parameters of Stability DAO - /// @dev For details see https://stabilitydao.gitbook.io/stability/stability-dao/governance#current-parameters - struct DaoParams { - /// @notice Minimal amount of xSTBL tokens required to have STBL_DAO tokens, decimals 18 - uint minimalPower; - /// @notice xSTBL instant exit penalty, decimals 1e4, i.e. 50_00 = 50% - /// Set 0 to use default value XSTBL.DEFAULT_SLASHING_PENALTY - uint exitPenalty; - /// @notice Min percent of power that a user should have to be able to create new proposal. Decimals 1e5, i.e. 50_000 = 50% - uint proposalThreshold; - /// @notice A percent of votes required to reach quorum for a proposal. Decimals 1e5, i.e. 20_000 = 20% - /// If the total number of votes is less than this percent, proposal is rejected - uint quorum; - /// @notice Inter-chain power allocation delay, i.e. 1 day - uint powerAllocationDelay; - } - - //region --------------------------------------- Read functions - /// @notice Current DAO config - function config() external view returns (DaoParams memory); - - /// @notice Address of xSTBL token - function xStbl() external view returns (address); - - /// @notice Address of xStaking contract - function xStaking() external view returns (address); - - /// @notice Minimal amount of xSTBL tokens required to have STBL_DAO tokens, decimals 18 - function minimalPower() external view returns (uint); - - /// @notice xSTBL instant exit penalty (slashing penalty), decimals 1e4, i.e. 50_00 = 50% - function exitPenalty() external view returns (uint); - - /// @notice Min percent of power that a user should have to be able to create a new proposal, decimals 1e5, i.e. 50_000 = 50% - function proposalThreshold() external view returns (uint); - - /// @notice A percent of votes required to reach quorum for a proposal, decimals 1e5, i.e. 20_000 = 20% - /// If the total number of votes is less than this percent, proposal is rejected - function quorum() external view returns (uint); - - /// @notice Inter-chain power allocation delay, i.e. 1 day - function powerAllocationDelay() external view returns (uint); - - /// @notice Get total power of a user. - /// The power = user's own (not-delegated) balance of STBL_DAO + balances of all users that delegated to him - /// If user has balance of staked xSTBL below minimalPower, his power is 0 - function getVotes(address user_) external view returns (uint); - - /// @notice Get delegation info of a user - /// @return delegatedTo The address to whom the user has delegated his voting power (or address(0) if not delegated) - /// @return delegators The list of addresses that have delegated their voting power to the user - function delegates(address user_) external view returns (address delegatedTo, address[] memory delegators); - - //endregion --------------------------------------- Read functions - - //region --------------------------------------- Write functions - /// @dev Init - function initialize(address platform_, address xStbl_, address xStaking_, DaoParams memory config_) external; - - /// @notice Update DAO config - /// XStaking.syncStabilityDAOBalances() must be called after changing of minimalPower value - /// @custom:restricted To multisig or governance - function updateConfig(DaoParams memory p) external; - - /// @custom:restricted To xStaking - function mint(address account, uint amount) external; - - /// @custom:restricted To xStaking - function burn(address account, uint amount) external; - - /// @notice Delegate all voting power to another user. - /// To remove delegation just delegate the power to yourself or to address(0). - /// @custom:restricted Anyone can call this function - function setPowerDelegation(address to) external; - - //endregion --------------------------------------- Write functions -} diff --git a/src/interfaces/ITokenOFTAdapter.sol b/src/interfaces/ITokenOFTAdapter.sol new file mode 100644 index 00000000..651fa80e --- /dev/null +++ b/src/interfaces/ITokenOFTAdapter.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {IOFTPausable} from "./IOFTPausable.sol"; + +interface ITokenOFTAdapter is IOFTPausable { + /// @param delegate_ The delegate capable of making OApp configurations inside of the endpoint. + /// Pass 0 to set multisig as the delegate. Owner (multisig) is able to change it using setDelegate. + function initialize(address platform_, address delegate_) external; +} diff --git a/src/interfaces/IXStaking.sol b/src/interfaces/IXStaking.sol index 95d46943..dde17067 100644 --- a/src/interfaces/IXStaking.sol +++ b/src/interfaces/IXStaking.sol @@ -16,39 +16,37 @@ interface IXStaking { event NewDuration(uint oldDuration, uint newDuration); - event InitializeStabilityDAO(address stblDao); - /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* WRITE FUNCTIONS */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ - /// @notice Deposits all xSTBL in the caller's wallet + /// @notice Deposits all xToken in the caller's wallet function depositAll() external; - /// @notice Deposit a specified amount of xSTBL + /// @notice Deposit a specified amount of xToken function deposit(uint amount) external; - /// @notice Withdraw all xSTBL and claim rewards + /// @notice Withdraw all xToken and claim rewards function withdrawAll() external; - /// @notice Withdraw a specified amount of xSTBL + /// @notice Withdraw a specified amount of xToken function withdraw(uint amount) external; /// @notice Claims pending rebase rewards function getReward() external; - /// @notice Used to notify pending xSTBL rebases and platform revenue share - /// @param amount The amount of STBL to be notified + /// @notice Used to notify pending xToken rebases and platform revenue share + /// @param amount The amount of main token to be notified function notifyRewardAmount(uint amount) external; /// @notice Change duration period function setNewDuration(uint) external; - /// @notice Updates STBL_DAO balances for the given users. + /// @notice Updates DAO-token balances for the given users. /// @custom:restricted Only operator - /// @dev If a user has less than the minimum staking power of xSTBL, his STBL_DAO balance will be zero. - /// Otherwise, the user receives 1 STBL_DAO for each 1 xSTBL staked. - function syncStabilityDAOBalances(address[] calldata users) external; + /// @dev If a user has less than the minimum staking power of xToken, his DAO-token balance will be zero. + /// Otherwise, the user receives 1 DAO-token for each 1 xToken staked. + function syncDAOBalances(address[] calldata users) external; /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* VIEW FUNCTIONS */ @@ -57,9 +55,9 @@ interface IXStaking { /// @notice Returns the last time the reward was modified or periodFinish if the reward has ended function lastTimeRewardApplicable() external view returns (uint); - /// @notice The address of the xSTBL token (staking/voting token) - /// @return xSTBL address - function xSTBL() external view returns (address); + /// @notice The address of the xToken token (staking/voting token) + /// @return xToken address + function xToken() external view returns (address); /// @notice Returns the total voting power (equal to total supply in the XStaking) function totalSupply() external view returns (uint); @@ -67,7 +65,7 @@ interface IXStaking { /// @notice Last time the rewards system was updated function lastUpdateTime() external view returns (uint); - /// @notice The amount of rewards per xSTBL + /// @notice The amount of rewards per xToken function rewardPerTokenStored() external view returns (uint); /// @notice When the 1800 seconds after notifying are up @@ -88,7 +86,7 @@ interface IXStaking { /// @return The stored rewards function storedRewardsPerUser(address user) external view returns (uint); - /// @notice Rewards per amount of xSTBL's staked + /// @notice Rewards per amount of xToken's staked function userRewardPerTokenStored(address user) external view returns (uint); /// @notice User's earned reward diff --git a/src/interfaces/IXSTBL.sol b/src/interfaces/IXToken.sol similarity index 70% rename from src/interfaces/IXSTBL.sol rename to src/interfaces/IXToken.sol index 9664cd3e..d6be3597 100644 --- a/src/interfaces/IXSTBL.sol +++ b/src/interfaces/IXToken.sol @@ -1,13 +1,13 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.28; -interface IXSTBL { +interface IXToken { /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* DATA TYPES */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ struct VestPosition { - /// @dev amount of xSTBL + /// @dev amount of xToken uint amount; /// @dev start unix timestamp uint start; @@ -36,19 +36,23 @@ interface IXSTBL { event ExemptionFrom(address indexed candidate, bool status, bool success); event ExemptionTo(address indexed candidate, bool status, bool success); event Rebase(address indexed caller, uint amount); + event SendToBridge(address indexed user, uint amount); + event ReceivedFromBridge(address indexed user, uint amount); + event XTokenName(string newName); + event XTokenSymbol(string newSymbol); /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* WRITE FUNCTIONS */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ - /// @dev Mints xSTBL for each STBL + /// @dev Mints xToken for each main-token function enter(uint amount_) external; /// @dev Exit instantly with a penalty - /// @param amount_ Amount of xSTBL to exit + /// @param amount_ Amount of xToken to exit function exit(uint amount_) external returns (uint exitedAmount); - /// @dev Vesting xSTBL --> STBL functionality + /// @dev Vesting xToken --> main token functionality function createVest(uint amount_) external; /// @dev Handles all situations regarding exiting vests @@ -60,9 +64,29 @@ interface IXSTBL { /// @notice Set exemption status for to address function setExemptionTo(address[] calldata exemptee, bool[] calldata exempt) external; + /// @notice Set or unset an address as XTokenBridge contract + /// @param bridge_ Address of the bridge contract + /// @param status_ Allow/disallow the bridge to call bridge-related functions + function setBridge(address bridge_, bool status_) external; + /// @notice Function called by the RevenueRouter to send the rebases once a week function rebase() external; + /// @notice Burn given {amount} of xToken for the given {user} and transfer main-token to the main-token-bridge. + /// The {user} will receive same amount of xToken on the different chain in return. + /// @custom:restricted This function can only be called by XTokenBridge contract. + function sendToBridge(address user, uint amount) external; + + /// @notice Mint given {amount} of xToken for the given {user} after receiving main-token from the main-token-bridge. + /// @custom:restricted This function can only be called by XTokenBridge contract. + function takeFromBridge(address user, uint amount) external; + + /// @notice Sets a new name for the token. + function setName(string calldata newName) external; + + /// @notice Sets a new symbol for the token. + function setSymbol(string calldata newSymbol) external; + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* VIEW FUNCTIONS */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ @@ -79,10 +103,10 @@ interface IXSTBL { /// @notice The maximum vesting length function MAX_VEST() external view returns (uint); - /// @notice STBL address - function STBL() external view returns (address); + /// @notice Main token address (i.e. STBL) + function token() external view returns (address); - /// @notice xSTBL staking contract + /// @notice xToken staking contract function xStaking() external view returns (address); /// @notice Revenue distributor contract @@ -99,4 +123,7 @@ interface IXSTBL { /// @notice The last period rebases were distributed function lastDistributedPeriod() external view returns (uint); + + /// @notice Checks if an address is set as XTokenBridge contract + function isBridge(address bridge_) external view returns (bool); } diff --git a/src/interfaces/IXTokenBridge.sol b/src/interfaces/IXTokenBridge.sol new file mode 100644 index 00000000..800ddf95 --- /dev/null +++ b/src/interfaces/IXTokenBridge.sol @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import {MessagingFee} from "@layerzerolabs/oft-evm/contracts/interfaces/IOFT.sol"; + +interface IXTokenBridge { + /// @notice Emitted when user sends xToken to another chain + /// @param userFrom The address of the user-sender + /// @param dstEid The destination chain endpoint ID + /// @param amount The amount of xToken to send (local decimals). + /// @param amountSentLD Amount of tokens ACTUALLY debited from the sender in local decimals (from OFTReceipt) + /// @param guidId The unique GUID identifier for the sent message (from MessagingReceipt) + /// @param nonce The nonce of the sent message (from MessagingReceipt) + /// @param nativeFee The amount of native fee paid for the cross-chain message + event XTokenSent( + address indexed userFrom, + uint32 indexed dstEid, + uint amount, + uint amountSentLD, + bytes32 indexed guidId, + uint64 nonce, + uint nativeFee + ); + + /// @notice Emitted when xToken is received from another chain + /// @param userTo The address of the recipient-user + /// @param srcEid The source chain endpoint ID + /// @param amount The amount of xToken received + /// @param guidId The unique GUID identifier for the received message + event Staked(address indexed userTo, uint32 indexed srcEid, uint amount, bytes32 guidId); + + event SetXTokenBridges(uint32[] dstEids, address[] xTokenBridges); + + error NotBridge(); + error ChainNotSupported(); + error IncorrectAmountReceivedFromXToken(); + error InvalidSenderXTokenBridge(); + error IncorrectReceiver(); + error UnauthorizedSender(); + error UntrustedOApp(); + error SenderPaused(); + error ZeroAmount(); + error IncorrectNativeValue(); + + /// @notice LayerZero Omnichain Fungible Token (OFT) bridge address + function bridge() external view returns (address); + + /// @notice xToken (i.e. xSTBL) address + function xToken() external view returns (address); + + /// @notice Get the xTokenBridge address for the given destination chain + /// @param dstEid_ Destination chain endpoint ID + function xTokenBridge(uint32 dstEid_) external view returns (address); + + /// @notice Quote the gas needed to pay for sending `amount` of xToken to given target chain. + /// Paying using ZRO token (Layer Zero token) is not supported. + /// @param dstEid_ Destination chain endpoint ID + /// @param amount Amount of tokens to send (local decimals) + /// @param options Additional options for the message. Use: + /// OptionsBuilder.addExecutorLzReceiveOption() + /// OptionsBuilder.addExecutorLzComposeOption() + /// Gas limit should take into account two calls on the destination chain: lzReceive() and lzCompose() + /// @return msgFee A `MessagingFee` struct containing the calculated gas fee. + function quoteSend( + uint32 dstEid_, + uint amount, + bytes calldata options + ) external view returns (MessagingFee memory msgFee); + + /// @notice Initialize the XTokenBridge + /// @param platform_ Address of the platform contract + /// @param bridge_ Address of the LayerZero OFT bridge contract + /// @param xToken_ Address of the xToken token contract + function initialize(address platform_, address bridge_, address xToken_) external; + + /// @notice Sets the xTokenBridge address for the given destination chain + /// @param dstEids_ Destination chain endpoint IDs + /// @param xTokenBridges_ Addresses of the xTokenBridge on the destination chain + function setXTokenBridge(uint32[] memory dstEids_, address[] memory xTokenBridges_) external; + + /// @notice Sends xToken to another chain + /// @dev The user must send enough native tokens to cover the cross-chain message fees. Use quoteSend to estimate it. + /// @param dstEid_ The target chain endpoint ID + /// @param amount The amount of xToken to send (local decimals) + /// @param msgFee The messaging fee struct obtained from quoteSend + /// @param options Additional options for the transfer (gas limit on target chain, etc.) + /// Use OptionsBuilder.addExecutorLzReceiveOption() to build options. + /// Gas limit should take into account two calls on the destination chain: lzReceive() and lzCompose() + function send(uint32 dstEid_, uint amount, MessagingFee calldata msgFee, bytes calldata options) external payable; + + /// @notice Salvage tokens mistakenly sent to this contract + /// @param token Address of the token to salvage + /// @param amount Amount of tokens to salvage + /// @param receiver Address to send the salvaged tokens to + function salvage(address token, uint amount, address receiver) external; + + /// @dev See OptionsBuilder.addExecutorLzReceiveOption.addExecutorLzComposeOption + /// @param gasLzReceive_ The gasLimit used on the lzReceive() function in the OApp. + /// @param valueLzReceive_ The msg.value passed to the lzReceive() function in the OApp (use 0). + /// @param _indexLzCompose The index for the lzCompose() function call (use 0). + /// @param gasLzCompose The gasLimit for the lzCompose() function call. + /// @param valueLzCompose_ The msg.value for the lzCompose() function call (use 0). + function buildOptions( + uint128 gasLzReceive_, + uint128 valueLzReceive_, + uint16 _indexLzCompose, + uint128 gasLzCompose, + uint128 valueLzCompose_ + ) external pure returns (bytes memory); +} diff --git a/src/periphery/BridgedPriceOracle.sol b/src/periphery/BridgedPriceOracle.sol new file mode 100644 index 00000000..4f97be37 --- /dev/null +++ b/src/periphery/BridgedPriceOracle.sol @@ -0,0 +1,147 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.22; + +import {OAppUpgradeable, Origin} from "@layerzerolabs/oapp-evm-upgradeable/contracts/oapp/OAppUpgradeable.sol"; +import {IControllable, Controllable} from "../core/base/Controllable.sol"; +import {IPlatform} from "../interfaces/IPlatform.sol"; +import {OAppEncodingLib} from "./libs/OAppEncodingLib.sol"; +import {IBridgedPriceOracle} from "../interfaces/IBridgedPriceOracle.sol"; +import {IAggregatorInterfaceMinimal} from "../integrations/chainlink/IAggregatorInterfaceMinimal.sol"; + +contract BridgedPriceOracle is Controllable, OAppUpgradeable, IBridgedPriceOracle { + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* CONSTANTS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @inheritdoc IControllable + string public constant VERSION = "1.0.0"; + + // keccak256(abi.encode(uint(keccak256("erc7201:stability.BridgedPriceOracle")) - 1)) & ~bytes32(uint(0xff)); + bytes32 internal constant _BRIDGED_PRICE_ORACLE_STORAGE_LOCATION = + 0x7de84bf9d24250450323fa11c7039f1c170849cf600decfdbf5e505497ab9b00; + + /// @dev Single slot + struct PriceInfo { + /// @notice Last price received from price aggregator in USD, 18 decimals + uint160 price; + + /// @notice Last time of {price} update + uint64 timestamp; + } + + /// @custom:storage-location erc7201:stability.BridgedPriceOracle + struct BridgedPriceOracleStorage { + string tokenSymbol; + + /// @notice Last stored price + PriceInfo lastPriceInfo; + } + + //region --------------------------------- Initializers + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* INITIALIZATION */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @notice Initialize with Endpoint V2 + constructor(address lzEndpoint_) OAppUpgradeable(lzEndpoint_) { + _disableInitializers(); + } + + /// @inheritdoc IBridgedPriceOracle + function initialize(address platform_, string memory tokenSymbol_, address delegate_) public initializer { + address _owner = IPlatform(platform_).multisig(); + + __Controllable_init(platform_); + __OApp_init(delegate_ == address(0) ? _owner : delegate_); + __Ownable_init(_owner); + + getBridgedPriceOracleStorage().tokenSymbol = tokenSymbol_; + } + + //endregion --------------------------------- Initializers + + //region --------------------------------- View + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* VIEW */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @inheritdoc IBridgedPriceOracle + function tokenSymbol() external view returns (string memory) { + return getBridgedPriceOracleStorage().tokenSymbol; + } + + /// @inheritdoc IBridgedPriceOracle + function getPriceUsd18() external view returns (uint price, uint priceTimestamp) { + PriceInfo memory priceInfo = getBridgedPriceOracleStorage().lastPriceInfo; + return (priceInfo.price, priceInfo.timestamp); + } + + /// @inheritdoc IAggregatorInterfaceMinimal + function latestAnswer() external view returns (int) { + // assume here that price aggregator always returns price in USD with 18 decimals + + // slither-disable-next-line unused-return + uint price = getBridgedPriceOracleStorage().lastPriceInfo.price; + + return int(price / 10 ** 10); + } + + /// @inheritdoc IAggregatorInterfaceMinimal + function decimals() external pure returns (uint8) { + return 8; + } + + //endregion --------------------------------- View + + //region --------------------------------- Actions + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* QApp receive logic */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @notice Invoked by OAppReceiver when EndpointV2.lzReceive is called + /// @dev origin_ Metadata (source chain, sender address, nonce) + /// @dev guid_ Global unique ID for tracking this message + /// @param message_ ABI-encoded bytes (the string we sent earlier) + /// @dev executor_ Executor address that delivered the message + /// @dev extraData_ Additional data from the Executor (unused here) + function _lzReceive( + Origin calldata, + /*origin*/ + bytes32, + /*guid_*/ + bytes calldata message_, + address, + /*executor_*/ + bytes calldata /*extraData_*/ + ) internal override { + BridgedPriceOracleStorage storage $ = getBridgedPriceOracleStorage(); + + // ---------------------- check sender + // struct Origin {uint32 srcEid; bytes32 sender; uint64 nonce;} + // we don't need to check sender explicitly + // assume that peers configuration doesn't allow untrusted senders (onlyPeer exception) + + // ---------------------- extract and verify message data + (uint16 messageFormat, uint160 price, uint64 timestamp) = OAppEncodingLib.unpackPriceUsd18(message_); + require(messageFormat == OAppEncodingLib.MESSAGE_FORMAT_PRICE_USD18_1, InvalidMessageFormat()); + + if ($.lastPriceInfo.timestamp > timestamp) { + // skip outdated price update + emit PriceUpdateSkipped(price, timestamp); + } else { + $.lastPriceInfo = PriceInfo({price: price, timestamp: timestamp}); + emit PriceUpdated(price, block.timestamp); + } + } + + //endregion --------------------------------- Actions + + //region --------------------------------- Internal logic + function getBridgedPriceOracleStorage() internal pure returns (BridgedPriceOracleStorage storage $) { + //slither-disable-next-line assembly + assembly { + $.slot := _BRIDGED_PRICE_ORACLE_STORAGE_LOCATION + } + } + //endregion --------------------------------- Internal logic +} diff --git a/src/periphery/PriceAggregatorOApp.sol b/src/periphery/PriceAggregatorOApp.sol new file mode 100644 index 00000000..43155850 --- /dev/null +++ b/src/periphery/PriceAggregatorOApp.sol @@ -0,0 +1,159 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.22; + +import { + OAppUpgradeable, + Origin, + MessagingFee +} from "@layerzerolabs/oapp-evm-upgradeable/contracts/oapp/OAppUpgradeable.sol"; +import {IControllable, Controllable} from "../core/base/Controllable.sol"; +import {IPlatform} from "../interfaces/IPlatform.sol"; +import {OAppEncodingLib} from "./libs/OAppEncodingLib.sol"; +import {IPriceAggregatorOApp} from "../interfaces/IPriceAggregatorOApp.sol"; +import {IPriceAggregator} from "../interfaces/IPriceAggregator.sol"; + +/// @notice Get price of given entity (vault or asset) from PriceAggregator +/// and send it to BridgetPriceOracle on the given chain through LayerZero OApp +/// by command of whitelisted address (backend). Each call sends single price +/// as packet of price value (usd, decimals 18) and timestamp of the price update. +contract PriceAggregatorOApp is Controllable, OAppUpgradeable, IPriceAggregatorOApp { + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* CONSTANTS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @inheritdoc IControllable + string public constant VERSION = "1.0.0"; + + // keccak256(abi.encode(uint(keccak256("erc7201:stability.PriceAggregatorOApp")) - 1)) & ~bytes32(uint(0xff)); + bytes32 internal constant _PRICE_AGGREGATOR_OAPP_STORAGE_LOCATION = + 0x03c24ae0f93ab26cb98c742598023b6422f9f4ca86d7754aa8be1070fd418e00; + + /// @custom:storage-location erc7201:stability.PriceAggregatorOApp + struct PriceAggregatorOAppStorage { + address entity; + + /// @notice All users trusted to send price updates + mapping(address sender => bool) whitelist; + } + + //region --------------------------------- Initializers + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* INITIALIZATION */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @notice Initialize with Endpoint V2 + constructor(address lzEndpoint_) OAppUpgradeable(lzEndpoint_) { + _disableInitializers(); + } + + /// @inheritdoc IPriceAggregatorOApp + function initialize(address platform_, address entity_, address delegate_) public initializer { + address _owner = IPlatform(platform_).multisig(); + + __Controllable_init(platform_); + __OApp_init(delegate_ == address(0) ? _owner : delegate_); + __Ownable_init(_owner); + + getPriceAggregatorOAppStorage().entity = entity_; + } + + //endregion --------------------------------- Initializers + + //region --------------------------------- View + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* VIEW */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @inheritdoc IPriceAggregatorOApp + function entity() external view returns (address) { + return getPriceAggregatorOAppStorage().entity; + } + + /// @inheritdoc IPriceAggregatorOApp + function isWhitelisted(address caller) external view returns (bool) { + PriceAggregatorOAppStorage storage $ = getPriceAggregatorOAppStorage(); + return $.whitelist[caller]; + } + + /// @inheritdoc IPriceAggregatorOApp + function quotePriceMessage( + uint32 dstEid_, + bytes memory options_, + bool payInLzToken_ + ) public view returns (MessagingFee memory fee) { + PriceAggregatorOAppStorage storage $ = getPriceAggregatorOAppStorage(); + // combineOptions (from OAppOptionsType3) merges enforced options set by the contract owner + // with any additional execution options provided by the caller + (bytes memory message,,) = _getPriceMessage($.entity); + fee = _quote(dstEid_, message, options_, payInLzToken_); + } + + //endregion --------------------------------- View + + //region --------------------------------- Actions + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* Restricted actions */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @inheritdoc IPriceAggregatorOApp + function changeWhitelist(address caller, bool whitelisted) external onlyGovernanceOrMultisig { + PriceAggregatorOAppStorage storage $ = getPriceAggregatorOAppStorage(); + $.whitelist[caller] = whitelisted; + + emit ChangeWhitelist(caller, whitelisted); + } + + /// @inheritdoc IPriceAggregatorOApp + function sendPriceMessage(uint32 dstEid_, bytes memory options_, MessagingFee memory fee_) external payable { + PriceAggregatorOAppStorage storage $ = getPriceAggregatorOAppStorage(); + require($.whitelist[msg.sender], NotWhitelisted()); + + (bytes memory message, uint price, uint timestamp) = _getPriceMessage($.entity); + _lzSend(dstEid_, message, options_, fee_, payable(msg.sender)); + + emit SendPriceMessage(dstEid_, price, timestamp); + } + + //endregion --------------------------------- Actions + + //region --------------------------------- Overrides + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* Overrides */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev This QApp does not expect to receive messages + function _lzReceive( + Origin calldata, + /*_origin*/ + bytes32, + /*_guid*/ + bytes calldata, + /*_message*/ + address, + /*_executor*/ + bytes calldata /*_extraData*/ + ) internal pure override { + revert UnsupportedOperation(); + } + + //endregion --------------------------------- Overrides + + //region --------------------------------- Internal logic + function getPriceAggregatorOAppStorage() internal pure returns (PriceAggregatorOAppStorage storage $) { + //slither-disable-next-line assembly + assembly { + $.slot := _PRICE_AGGREGATOR_OAPP_STORAGE_LOCATION + } + } + + function _getPriceMessage(address entity_) + internal + view + returns (bytes memory message, uint price, uint timestamp) + { + // slither-disable-next-line unused-return + (price, timestamp,) = IPriceAggregator(IPlatform(platform()).priceAggregator()).price(entity_); + return (OAppEncodingLib.packPriceUsd18(price, timestamp), price, timestamp); + } + //endregion --------------------------------- Internal logic +} diff --git a/src/periphery/libs/OAppEncodingLib.sol b/src/periphery/libs/OAppEncodingLib.sol new file mode 100644 index 00000000..2a85e550 --- /dev/null +++ b/src/periphery/libs/OAppEncodingLib.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +library OAppEncodingLib { + /// @notice Format of the message containing price in USD with 18 decimals + uint16 internal constant MESSAGE_FORMAT_PRICE_USD18_1 = 1; + + function packPriceUsd18(uint price, uint timestamp) internal pure returns (bytes memory) { + bytes32 serialized = bytes32( + (uint(MESSAGE_FORMAT_PRICE_USD18_1) << 240) | (uint(uint160(price)) << 80) | (uint(uint64(timestamp)) << 16) + ); + return abi.encodePacked(serialized); + } + + /// @dev calldata is used to reduce gas consumption of lzReceive (from 29894 to 29703) + function unpackPriceUsd18(bytes calldata message) + internal + pure + returns (uint16 format, uint160 price, uint64 timestamp) + { + // assume here that message length >= 32 here + bytes32 serialized = abi.decode(message, (bytes32)); + + uint raw = uint(serialized); + + format = uint16(raw >> 240); + price = uint160(raw >> 80); + timestamp = uint64((raw >> 16) & ((uint(1) << 64) - 1)); + } +} diff --git a/src/test/MockXToken.sol b/src/test/MockXToken.sol new file mode 100644 index 00000000..85a0899f --- /dev/null +++ b/src/test/MockXToken.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +/// @notice Mock to check XTokenBridge.send bad paths +contract MockXToken { + using SafeERC20 for IERC20; + + address internal _token; + uint internal _amountToSend; + + constructor(address token_, uint amountToSend) { + _token = token_; + _amountToSend = amountToSend; + } + + function token() external view returns (address) { + return _token; + } + + function sendToBridge( + address user, + uint /*amount*/ + ) external { + IERC20(_token).safeTransfer(user, _amountToSend); + } +} diff --git a/src/tokenomics/BridgedToken.sol b/src/tokenomics/BridgedToken.sol new file mode 100755 index 00000000..1b664437 --- /dev/null +++ b/src/tokenomics/BridgedToken.sol @@ -0,0 +1,181 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.22; + +import {OFTUpgradeable} from "@layerzerolabs/oft-evm-upgradeable/contracts/oft/OFTUpgradeable.sol"; +import {IControllable, Controllable} from "../core/base/Controllable.sol"; +import {IPlatform} from "../interfaces/IPlatform.sol"; +import {IBridgedToken} from "../interfaces/IBridgedToken.sol"; +import {IOFTPausable} from "../interfaces/IOFTPausable.sol"; +import {ERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; +import {OptionsBuilder} from "@layerzerolabs/oapp-evm/contracts/oapp/libs/OptionsBuilder.sol"; + +/// @notice Omnichain Fungible Token - bridged version of main-token from Sonic to other chains +/// Changelog: +/// - 1.0.2: Add buildOptions function +/// - 1.0.1: Add setName and setSymbol functions +contract BridgedToken is Controllable, OFTUpgradeable, IBridgedToken { + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* CONSTANTS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @inheritdoc IControllable + string public constant VERSION = "1.0.2"; + + // keccak256(abi.encode(uint(keccak256("erc7201:stability.BridgedToken")) - 1)) & ~bytes32(uint(0xff)); + bytes32 internal constant BRIDGED_TOKEN_STORAGE_LOCATION = + 0x5908ba930cd8810ead4eba737803862bca8ae4a4891cfeedea00ad638eaee100; + + /// @custom:storage-location erc7201:stability.BridgedToken + struct BridgedTokenStorage { + /// @notice Paused state for addresses + mapping(address => bool) paused; + /// @dev Changed ERC20 name + string changedName; + /// @dev Changed ERC20 symbol + string changedSymbol; + } + + //region --------------------------------- Initializers + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* INITIALIZATION */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + constructor(address lzEndpoint_) OFTUpgradeable(lzEndpoint_) { + _disableInitializers(); + } + + /// @inheritdoc IBridgedToken + function initialize( + address platform_, + string memory name_, + string memory symbol_, + address delegate_ + ) public initializer { + address _owner = IPlatform(platform_).multisig(); + + __Controllable_init(platform_); + __OFT_init(name_, symbol_, delegate_); + __Ownable_init(_owner); + } + + //endregion --------------------------------- Initializers + + //region --------------------------------- Restricted actions + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* RESTRICTED ACTIONS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @inheritdoc IOFTPausable + function setPaused(address account, bool paused_) external onlyOperator { + BridgedTokenStorage storage $ = getBridgedTokenStorage(); + $.paused[account] = paused_; + + emit Pause(account, paused_); + } + + /// @inheritdoc IBridgedToken + function setName(string calldata newName) external onlyOperator { + BridgedTokenStorage storage $ = getBridgedTokenStorage(); + $.changedName = newName; + emit BridgedTokenName(newName); + } + + /// @inheritdoc IBridgedToken + function setSymbol(string calldata newSymbol) external onlyOperator { + BridgedTokenStorage storage $ = getBridgedTokenStorage(); + $.changedSymbol = newSymbol; + emit BridgedTokenSymbol(newSymbol); + } + + //endregion --------------------------------- Restricted actions + + //region --------------------------------- View + + /// @inheritdoc IOFTPausable + function paused(address account_) external view returns (bool) { + return getBridgedTokenStorage().paused[account_]; + } + + /// @inheritdoc ERC20Upgradeable + function name() public view override returns (string memory) { + BridgedTokenStorage storage $ = getBridgedTokenStorage(); + string memory changedName = $.changedName; + if (bytes(changedName).length != 0) { + return changedName; + } + return super.name(); + } + + /// @inheritdoc ERC20Upgradeable + function symbol() public view override returns (string memory) { + BridgedTokenStorage storage $ = getBridgedTokenStorage(); + string memory changedSymbol = $.changedSymbol; + if (bytes(changedSymbol).length != 0) { + return changedSymbol; + } + return super.symbol(); + } + + /// @inheritdoc IOFTPausable + function buildOptions(uint128 gas_, uint128 value_) external pure returns (bytes memory) { + return OptionsBuilder.addExecutorLzReceiveOption(OptionsBuilder.newOptions(), gas_, value_); + } + + //endregion --------------------------------- View + + //region --------------------------------- Overrides + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* OVERRIDES */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + function _checkOwner() internal view override { + _requireMultisig(); + } + + /// @dev Paused accounts cannot send tokens + function _update(address from, address to, uint value) internal virtual override { + _requireNotPaused(from); + + super._update(from, to, value); + } + + /// @dev Paused accounts cannot send tokens + function _debit( + address from_, + uint amountLD_, + uint minAmountLD_, + uint32 dstEid_ + ) internal virtual override returns (uint amountSentLD, uint amountReceivedLD) { + _requireNotPaused(from_); + + return super._debit(from_, amountLD_, minAmountLD_, dstEid_); + } + + /// @dev Paused accounts cannot receive tokens + function _credit( + address to_, + uint amountLD_, + uint32 srcEid_ + ) internal virtual override returns (uint amountReceivedLD) { + _requireNotPaused(to_); + + return super._credit(to_, amountLD_, srcEid_); + } + + //endregion --------------------------------- Overrides + + //region --------------------------------- Internal logic + function getBridgedTokenStorage() internal pure returns (BridgedTokenStorage storage $) { + //slither-disable-next-line assembly + assembly { + $.slot := BRIDGED_TOKEN_STORAGE_LOCATION + } + } + + /// @notice Reverts if the account is paused OR the whole bridge is paused + function _requireNotPaused(address account) internal view { + BridgedTokenStorage storage $ = getBridgedTokenStorage(); + require(!$.paused[account] && !$.paused[address(this)], Paused()); + } + + //endregion --------------------------------- Internal logic +} diff --git a/src/tokenomics/DAO.sol b/src/tokenomics/DAO.sol new file mode 100644 index 00000000..9db81017 --- /dev/null +++ b/src/tokenomics/DAO.sol @@ -0,0 +1,410 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import {Controllable, IControllable} from "../core/base/Controllable.sol"; +import {ERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; +import { + ERC20BurnableUpgradeable +} from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20BurnableUpgradeable.sol"; +import {IDAO} from "../interfaces/IDAO.sol"; +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +import {EnumerableMap} from "@openzeppelin/contracts/utils/structs/EnumerableMap.sol"; +import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; +import {ConstantsLib} from "../core/libs/ConstantsLib.sol"; +import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; + +/// @title Stability DAO Token contract +/// Amount of tokens for each user represents their voting power in the DAO. +/// Only users with high enough amount of staked xToken have DAO-tokens. +/// Tokens are non-transferable and can be only minted and burned by XStaking contract. +/// @author Omriss (https://github.com/omriss) +/// Changelog: +/// 1.1.0: getVotes returns total voting power for all chains. Add setOtherChainsPowers + whitelist. +/// initialize() has two new params: name and symbol. Contract renamed from StabilityDAO to DAO +/// Allow to forbid delegation - #424 +/// Add setName and setSymbol functions. +/// 1.0.1: userPower is renamed to getVotes (compatibility with OpenZeppelin's ERC20Votes) - #423 +contract DAO is Controllable, ERC20Upgradeable, ERC20BurnableUpgradeable, ReentrancyGuardUpgradeable, IDAO { + using EnumerableMap for EnumerableMap.AddressToUintMap; + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* CONSTANTS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @inheritdoc IControllable + string public constant VERSION = "1.1.0"; + + /// @dev Name "erc7201:stability.StabilityDAO" is used historically + // keccak256(abi.encode(uint(keccak256("erc7201:stability.StabilityDAO")) - 1)) & ~bytes32(uint(0xff)); + bytes32 private constant _DAO_TOKEN_STORAGE_LOCATION = + 0xb41400b8ab7d5c4f4647f6397fc72c137345511eb9c9a0082de7fe729c2ae200; // StabilityDAO name is used historically + + /// @dev Same to xToken.DENOMINATOR + uint internal constant DENOMINATOR_XTOKEN = 10_000; + + //region ----------------------------------- Data types + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* DATA TYPES */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @notice Voting power of the users on other chains + /// @dev We keep it in a separate struct to be able to update all data by single write operation + struct OtherChainsPowers { + /// @notice Voting powers of users on other chains + EnumerableMap.AddressToUintMap powers; + } + + /// @custom:storage-location erc7201:stability.StabilityDAO + struct DaoStorage { + /// @dev Mapping is used to be able to add new fields to DaoParams struct in future, only config[0] is used + mapping(uint => DaoParams) config; + /// @notice Address of xToken + address xToken; + /// @notice Address of xStaking contract + address xStaking; + /// @notice Address to which a user has delegated his vote power + mapping(address user => address) delegatedTo; + /// @notice Set of addresses that have delegated their vote power to a user + mapping(address user => EnumerableSet.AddressSet) delegators; + + /// @notice Epoch of the last update of OtherChainsPowers. Each call of updateOtherChainsPowers increases it. + /// @dev It's timestamp of the block when otherChainsPowers were updated last time + uint otherChainsEpoch; + + /// @notice Active instance of OtherChainsPowers stored for key = {otherChainsEpoch} + /// Map is used to update all data by single write operation + mapping(uint epoch => OtherChainsPowers) otherChainsPowers; + + /// @notice Whitelist for addresses allowed to update OtherChainsPowers + mapping(address user => bool allowed) otherChainsPowersWhitelist; + + /// @notice Is delegation of voting power on the current chain forbidden + /// Basically delegation must be forbidden on all chains except main one (sonic for Stability) + bool delegationForbidden; + + /// @dev Changed ERC20 name + string changedName; + /// @dev Changed ERC20 symbol + string changedSymbol; + } + + //endregion ----------------------------------- Data types + + //region ----------------------------------- Initialization and modifiers + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* MODIFIERS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + modifier onlyXStaking() virtual { + _requireXStaking(); + _; + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* INITIALIZATION */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @inheritdoc IDAO + function initialize( + address platform_, + address xToken_, + address xStaking_, + DaoParams memory p, + string memory name_, + string memory symbol_ + ) public initializer { + __Controllable_init(platform_); + __ERC20_init(name_, symbol_); // i.e. "Stability DAO", "STBL_DAO" + DaoStorage storage $ = _getDaoStorage(); + $.xStaking = xStaking_; + $.xToken = xToken_; + $.config[0] = p; + } + + //endregion ----------------------------------- Initialization and modifiers + + //region ----------------------------------- Restricted actions + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* RESTRICTED ACTIONS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @inheritdoc IDAO + function mint(address account, uint amount) external onlyXStaking { + _mint(account, amount); + } + + /// @inheritdoc IDAO + function burn(address account, uint amount) external onlyXStaking { + _burn(account, amount); + } + + /// @inheritdoc IDAO + function updateConfig(DaoParams memory p) external onlyGovernanceOrMultisig { + DaoStorage storage $ = _getDaoStorage(); + + require(p.exitPenalty < DENOMINATOR_XTOKEN, WrongValue()); + require(p.quorum < ConstantsLib.DENOMINATOR && p.proposalThreshold < ConstantsLib.DENOMINATOR, WrongValue()); + + $.config[0] = p; + + emit ConfigUpdated(p); + } + + /// @inheritdoc IDAO + function setPowerDelegation(address to) external nonReentrant { + // anyone can call this function + + DaoStorage storage $ = _getDaoStorage(); + + if (to == msg.sender || to == address(0)) { + address delegatee = $.delegatedTo[msg.sender]; + $.delegatedTo[msg.sender] = address(0); + + //slither-disable-next-line unused-return + EnumerableSet.remove($.delegators[delegatee], msg.sender); + + emit UnDelegatePower(msg.sender, to); + } else { + require(!$.delegationForbidden, DelegationForbiddenOnTheChain()); + require($.delegatedTo[msg.sender] == address(0), AlreadyDelegated()); + $.delegatedTo[msg.sender] = to; + + //slither-disable-next-line unused-return + EnumerableSet.add($.delegators[to], msg.sender); + + emit DelegatePower(msg.sender, to); + } + } + + /// @inheritdoc IDAO + function setWhitelistedForOtherChainsPowers(address user, bool whitelisted) external onlyGovernanceOrMultisig { + DaoStorage storage $ = _getDaoStorage(); + $.otherChainsPowersWhitelist[user] = whitelisted; + + emit WhitelistOtherChainsPowers(user, whitelisted); + } + + /// @inheritdoc IDAO + function updateOtherChainsPowers(address[] memory users, uint[] memory powers) external { + DaoStorage storage $ = _getDaoStorage(); + require($.otherChainsPowersWhitelist[msg.sender], NotOtherChainsPowersWhitelisted()); + + uint len = users.length; + require(len == powers.length, IControllable.IncorrectArrayLength()); + + uint epoch = block.timestamp; + require(epoch > $.otherChainsEpoch, WrongValue()); // just for safety forbid double update in the same block + $.otherChainsEpoch = epoch; + + OtherChainsPowers storage poc = $.otherChainsPowers[epoch]; + + for (uint i; i < len; ++i) { + // slither-disable-next-line unused-return + poc.powers.set(users[i], powers[i]); + } + + emit PowersOtherChainsUpdated(block.timestamp); + } + + /// @inheritdoc IDAO + function setDelegationForbidden(bool forbidden) external onlyGovernanceOrMultisig { + DaoStorage storage $ = _getDaoStorage(); + $.delegationForbidden = forbidden; + + emit SetDelegationForbiddenOnTheChain(forbidden); + } + + /// @inheritdoc IDAO + function setName(string calldata newName) external onlyOperator { + DaoStorage storage $ = _getDaoStorage(); + $.changedName = newName; + emit DaoName(newName); + } + + /// @inheritdoc IDAO + function setSymbol(string calldata newSymbol) external onlyOperator { + DaoStorage storage $ = _getDaoStorage(); + $.changedSymbol = newSymbol; + emit DaoSymbol(newSymbol); + } + + //endregion ----------------------------------- Restricted actions + + //region ----------------------------------- ERC20 hooks + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* ERC20 HOOKS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev The token is not transferable, only minting and burning is allowed + function _update(address from, address to, uint value) internal override { + require(from == address(0) || to == address(0), NonTransferable()); + + super._update(from, to, value); + } + + //endregion ----------------------------------- ERC20 hooks + + //region ----------------------------------- View functions + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* VIEW FUNCTIONS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @inheritdoc IDAO + function config() public view returns (DaoParams memory) { + return _getDaoStorage().config[0]; + } + + /// @inheritdoc IDAO + function xToken() public view returns (address) { + return _getDaoStorage().xToken; + } + + /// @inheritdoc IDAO + function xStaking() public view returns (address) { + return _getDaoStorage().xStaking; + } + + /// @inheritdoc IDAO + function minimalPower() external view returns (uint) { + return _getDaoStorage().config[0].minimalPower; + } + + /// @inheritdoc IDAO + function exitPenalty() external view returns (uint) { + return _getDaoStorage().config[0].exitPenalty; + } + + /// @inheritdoc IDAO + function proposalThreshold() external view returns (uint) { + return _getDaoStorage().config[0].proposalThreshold; + } + + /// @inheritdoc IDAO + function quorum() external view returns (uint) { + return _getDaoStorage().config[0].quorum; + } + + /// @inheritdoc IDAO + function powerAllocationDelay() external view returns (uint) { + return _getDaoStorage().config[0].powerAllocationDelay; + } + + /// @inheritdoc IDAO + function getVotes(address user_) public view returns (uint votes) { + DaoStorage storage $ = _getDaoStorage(); + if ($.delegatedTo[user_] == address(0)) { + (uint localPower, uint otherPower) = _getPowers($, user_); + votes = localPower + otherPower; + } + + if (!$.delegationForbidden) { + EnumerableMap.AddressToUintMap storage _otherPowers = $.otherChainsPowers[$.otherChainsEpoch].powers; + address[] memory delegators = EnumerableSet.values($.delegators[user_]); + uint len = delegators.length; + for (uint i; i < len; ++i) { + votes += balanceOf(delegators[i]); + votes += _otherPowers.contains(delegators[i]) ? _otherPowers.get(delegators[i]) : 0; + } + } + + return votes; + } + + /// @inheritdoc IDAO + function getPowers(address user_) external view returns (uint localPower, uint otherPower) { + (localPower, otherPower) = _getPowers(_getDaoStorage(), user_); + } + + /// @inheritdoc IDAO + function delegates(address user_) external view returns (address delegatedTo, address[] memory delegators) { + DaoStorage storage $ = _getDaoStorage(); + return ($.delegatedTo[user_], EnumerableSet.values($.delegators[user_])); + } + + /// @inheritdoc IDAO + function getOtherChainsPowers() + external + view + returns (uint timestamp, address[] memory users, uint[] memory powers) + { + DaoStorage storage $ = _getDaoStorage(); + uint epoch = $.otherChainsEpoch; + OtherChainsPowers storage poc = $.otherChainsPowers[$.otherChainsEpoch]; + + uint len = poc.powers.length(); + + users = new address[](len); + powers = new uint[](len); + for (uint i; i < len; ++i) { + (address user, uint power) = poc.powers.at(i); + users[i] = user; + powers[i] = power; + } + + timestamp = epoch; + } + + /// @inheritdoc IDAO + function isWhitelistedForOtherChainsPowers(address user_) external view returns (bool) { + return _getDaoStorage().otherChainsPowersWhitelist[user_]; + } + + /// @inheritdoc IDAO + function delegationForbidden() external view returns (bool) { + return _getDaoStorage().delegationForbidden; + } + + /// @inheritdoc ERC20Upgradeable + function name() public view override(ERC20Upgradeable, IERC20Metadata) returns (string memory) { + DaoStorage storage $ = _getDaoStorage(); + string memory changedName = $.changedName; + if (bytes(changedName).length != 0) { + return changedName; + } + return super.name(); + } + + /// @inheritdoc ERC20Upgradeable + function symbol() public view override(ERC20Upgradeable, IERC20Metadata) returns (string memory) { + DaoStorage storage $ = _getDaoStorage(); + string memory changedSymbol = $.changedSymbol; + if (bytes(changedSymbol).length != 0) { + return changedSymbol; + } + return super.symbol(); + } + + //endregion ----------------------------------- View functions + + //region ----------------------------------- Voting power calculation + + /// @notice Get powers of the given user. + /// @param user_ The address of the user. + /// @return localPower Power on the current chain. This power can be delegated to other user (delegates.delegatedTo}. + /// @return otherPower Power on other chains. This power can be delegated to other user (delegates.delegatedTo}. + function _getPowers(DaoStorage storage $, address user_) internal view returns (uint localPower, uint otherPower) { + EnumerableMap.AddressToUintMap storage _otherPowers = $.otherChainsPowers[$.otherChainsEpoch].powers; + + localPower = balanceOf(user_); + otherPower = _otherPowers.contains(user_) ? _otherPowers.get(user_) : 0; + + return (localPower, otherPower); + } + //endregion ----------------------------------- Voting power calculation + + //region ----------------------------------- Internal logic + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* INTERNAL LOGIC */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + function _requireXStaking() internal view { + require(_getDaoStorage().xStaking == msg.sender, IncorrectMsgSender()); + } + + function _getDaoStorage() internal pure returns (DaoStorage storage $) { + //slither-disable-next-line assembly + assembly { + $.slot := _DAO_TOKEN_STORAGE_LOCATION + } + } + //endregion ----------------------------------- Internal logic +} diff --git a/src/tokenomics/Recovery.sol b/src/tokenomics/Recovery.sol index 69c5263f..fe6782ff 100644 --- a/src/tokenomics/Recovery.sol +++ b/src/tokenomics/Recovery.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.28; import {RecoveryLib} from "./libs/RecoveryLib.sol"; import {Controllable, IControllable, IPlatform} from "../core/base/Controllable.sol"; -import {IRecovery} from "../interfaces/IRecovery.sol"; +import {IRecovery, IRecoveryBase} from "../interfaces/IRecovery.sol"; import {IUniswapV3SwapCallback} from "../integrations/uniswapv3/IUniswapV3SwapCallback.sol"; import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; import {ISwapper} from "../interfaces/ISwapper.sol"; @@ -12,6 +12,7 @@ import {IPriceReader} from "../interfaces/IPriceReader.sol"; /// @title Recovery contract to swap assets on recovery tokens in recovery pools /// @author dvpublic (https://github.com/dvpublic) /// Changelog: +/// 1.2.3: IRecoveryBase was added - #424 /// 1.2.2: add swapExplicitly, selectPool tries to avoid pools with price = 1 - #427 /// 1.2.1: replace event SwapAssets by event SwapAssets2 /// 1.2.0: getListTokensToSwap excludes meta vault tokens, add getListRegisteredTokens, fix getPoolWithMinPrice logic @@ -26,13 +27,13 @@ contract Recovery is Controllable, IRecovery, IUniswapV3SwapCallback { /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ /// @inheritdoc IControllable - string public constant VERSION = "1.2.2"; + string public constant VERSION = "1.2.3"; /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* INITIALIZATION */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ - /// @inheritdoc IRecovery + /// @inheritdoc IRecoveryBase function initialize(address platform_) public initializer { __Controllable_init(platform_); } @@ -150,7 +151,7 @@ contract Recovery is Controllable, IRecovery, IUniswapV3SwapCallback { /* Actions */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ - /// @inheritdoc IRecovery + /// @inheritdoc IRecoveryBase function registerAssets(address[] memory tokens) external override onlyWhitelisted { RecoveryLib.registerAssets(tokens); } diff --git a/src/tokenomics/RecoveryRelayer.sol b/src/tokenomics/RecoveryRelayer.sol new file mode 100644 index 00000000..5652dc8c --- /dev/null +++ b/src/tokenomics/RecoveryRelayer.sol @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import {RecoveryRelayerLib} from "./libs/RecoveryRelayerLib.sol"; +import {Controllable, IControllable, IPlatform} from "../core/base/Controllable.sol"; +import {IRecoveryRelayer, IRecoveryBase} from "../interfaces/IRecoveryRelayer.sol"; +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; + +// import {ISwapper} from "../interfaces/ISwapper.sol"; +// import {IPriceReader} from "../interfaces/IPriceReader.sol"; + +/// @title Contract to collect recovery amounts on not-main chains and transfer them to the main chain +/// @author omriss (https://github.com/omriss) +contract RecoveryRelayer is Controllable, IRecoveryRelayer { + using EnumerableSet for EnumerableSet.AddressSet; + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* CONSTANTS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @inheritdoc IControllable + string public constant VERSION = "1.0.0"; + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* INITIALIZATION */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @inheritdoc IRecoveryBase + function initialize(address platform_) public initializer { + __Controllable_init(platform_); + } + + modifier onlyWhitelisted() { + _onlyWhitelisted(); + _; + } + + //region ----------------------------------- View + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* VIEW FUNCTIONS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @inheritdoc IRecoveryRelayer + function threshold(address token) external view override returns (uint) { + RecoveryRelayerLib.RecoveryRelayerStorage storage $ = RecoveryRelayerLib.getRecoveryRelayerStorage(); + return $.tokenThresholds[token]; + } + + /// @inheritdoc IRecoveryRelayer + function whitelisted(address operator_) public view override returns (bool) { + if (IPlatform(platform()).multisig() == operator_) { + return true; + } + + RecoveryRelayerLib.RecoveryRelayerStorage storage $ = RecoveryRelayerLib.getRecoveryRelayerStorage(); + return $.whitelistOperators[operator_]; + } + + /// @inheritdoc IRecoveryRelayer + function isTokenRegistered(address token) external view override returns (bool) { + RecoveryRelayerLib.RecoveryRelayerStorage storage $ = RecoveryRelayerLib.getRecoveryRelayerStorage(); + return $.registeredTokens.contains(token); + } + + /// @inheritdoc IRecoveryRelayer + function getListTokensToSwap() external view returns (address[] memory tokens) { + RecoveryRelayerLib.RecoveryRelayerStorage storage $ = RecoveryRelayerLib.getRecoveryRelayerStorage(); + return RecoveryRelayerLib.getListTokensToSwap($); + } + + /// @inheritdoc IRecoveryRelayer + function getListRegisteredTokens() external view returns (address[] memory tokens) { + RecoveryRelayerLib.RecoveryRelayerStorage storage $ = RecoveryRelayerLib.getRecoveryRelayerStorage(); + return RecoveryRelayerLib.getListRegisteredTokens($); + } + + //endregion ----------------------------------- View + + //region ----------------------------------- Restricted actions + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* RESTRICTED ACTIONS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @inheritdoc IRecoveryRelayer + function setThresholds(address[] memory tokens, uint[] memory thresholds) external onlyOperator { + RecoveryRelayerLib.setThresholds(tokens, thresholds); + } + + /// @inheritdoc IRecoveryRelayer + function changeWhitelist(address operator_, bool add_) external onlyOperator { + RecoveryRelayerLib.changeWhitelist(operator_, add_); + } + + //endregion ----------------------------------- Restricted actions + + //region ----------------------------------- Actions + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* Actions */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @inheritdoc IRecoveryBase + function registerAssets(address[] memory tokens) external override onlyWhitelisted { + RecoveryRelayerLib.registerAssets(tokens); + } + + //endregion ----------------------------------- Actions + + function _onlyWhitelisted() internal view { + require(whitelisted(msg.sender), RecoveryRelayerLib.NotWhitelisted()); + } +} diff --git a/src/tokenomics/RevenueRouter.sol b/src/tokenomics/RevenueRouter.sol index c8604419..d9dcc7de 100644 --- a/src/tokenomics/RevenueRouter.sol +++ b/src/tokenomics/RevenueRouter.sol @@ -11,14 +11,16 @@ import {IPool} from "../integrations/aave/IPool.sol"; import {IRevenueRouter, EnumerableMap, EnumerableSet} from "../interfaces/IRevenueRouter.sol"; import {IStabilityVault} from "../interfaces/IStabilityVault.sol"; import {ISwapper} from "../interfaces/ISwapper.sol"; -import {IXSTBL} from "../interfaces/IXSTBL.sol"; +import {IXToken} from "../interfaces/IXToken.sol"; import {IXStaking} from "../interfaces/IXStaking.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {IHardWorker} from "../interfaces/IHardWorker.sol"; -import {IRecovery} from "../interfaces/IRecovery.sol"; +import {IRecoveryBase} from "../interfaces/IRecoveryBase.sol"; /// @title Platform revenue distributor /// Changelog: +/// 1.8.0: renaming (STBL => main-token, xSTBL => xToken), xShare = 100% by default. +/// Add setAddresses, getXShare. RevenueRouter uses IRecoveryBase instead of IRecovery - #426 /// 1.7.1: add addresses() /// 1.7.0: improve /// 1.6.0: send 20% of earned assets to Recovery @@ -37,11 +39,14 @@ contract RevenueRouter is Controllable, IRevenueRouter { /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ /// @inheritdoc IControllable - string public constant VERSION = "1.7.1"; + string public constant VERSION = "1.8.0"; uint internal constant RECOVER_PERCENTAGE = 20_000; // 20% uint internal constant DENOMINATOR = 100_000; // 100% + /// @notice Count of addresses in addresses() and setAddresses + uint internal constant COUNT_ADDRESSES = 4; + // keccak256(abi.encode(uint256(keccak256("erc7201:stability.RevenueRouter")) - 1)) & ~bytes32(uint256(0xff)) bytes32 private constant REVENUE_ROUTER_STORAGE_LOCATION = 0x052d2762d037d7d0dd41be56f750d8d5de9f07d940d686a3b9365e8e49143600; @@ -50,14 +55,14 @@ contract RevenueRouter is Controllable, IRevenueRouter { /* INITIALIZATION */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ - function initialize(address platform_, address xStbl_, address feeTreasury_) external initializer { + function initialize(address platform_, address xToken_, address feeTreasury_) external initializer { __Controllable_init(platform_); RevenueRouterStorage storage $ = _getRevenueRouterStorage(); - if (xStbl_ != address(0)) { - $.stbl = IXSTBL(xStbl_).STBL(); - $.xStbl = xStbl_; - $.xStaking = IXSTBL(xStbl_).xStaking(); - $.xShare = 50_000; + if (xToken_ != address(0)) { + $.token = IXToken(xToken_).token(); + $.xToken = xToken_; + $.xStaking = IXToken(xToken_).xStaking(); + $.xShare = 100_000; } $.feeTreasury = feeTreasury_; $.activePeriod = getPeriod(); @@ -124,6 +129,19 @@ contract RevenueRouter is Controllable, IRevenueRouter { function setXShare(uint newShare) external onlyGovernanceOrMultisig { RevenueRouterStorage storage $ = _getRevenueRouterStorage(); $.xShare = newShare; + + emit SetXShare(newShare); + } + + /// @inheritdoc IRevenueRouter + function setAddresses(address[] memory addresses_) external onlyGovernanceOrMultisig { + RevenueRouterStorage storage $ = _getRevenueRouterStorage(); + $.token = addresses_[0]; + $.xToken = addresses_[1]; + $.xStaking = addresses_[2]; + $.feeTreasury = addresses_[3]; + + emit SetAddresses(addresses_); } /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ @@ -138,10 +156,10 @@ contract RevenueRouter is Controllable, IRevenueRouter { $.activePeriod = _activePeriod; newPeriod = _activePeriod; uint periodEnded = newPeriod - 1; - address _xstbl = $.xStbl; - if (_xstbl != address(0)) { - // process PvP rewards (100% xSTBL exit fees) - IXSTBL(_xstbl).rebase(); + address _xToken = $.xToken; + if (_xToken != address(0)) { + // process PvP rewards (100% xToken exit fees) + IXToken(_xToken).rebase(); // process core Unit revenue uint _pendingRevenue = $.pendingRevenue; @@ -160,7 +178,7 @@ contract RevenueRouter is Controllable, IRevenueRouter { // put week rewards to XStaking users if (_pendingRevenue != 0) { address _xStaking = $.xStaking; - IERC20($.stbl).approve(_xStaking, _pendingRevenue); + IERC20($.token).approve(_xStaking, _pendingRevenue); IXStaking(_xStaking).notifyRewardAmount(_pendingRevenue); } @@ -193,8 +211,8 @@ contract RevenueRouter is Controllable, IRevenueRouter { if ($.units[unitIndex].unitType == UnitType.AaveMarkets) { (address[] memory outAssets, uint[] memory amounts) = IFeeTreasury($.units[unitIndex].feeTreasury).harvest(); ISwapper swapper = ISwapper(IPlatform(platform()).swapper()); - address stbl = $.stbl; - uint stblBalanceWas = IERC20(stbl).balanceOf(address(this)); + address _mainToken = $.token; + uint mainTokenBalanceWas = IERC20(_mainToken).balanceOf(address(this)); for (uint i; i < outAssets.length; ++i) { address asset = IAToken(outAssets[i]).UNDERLYING_ASSET_ADDRESS(); @@ -203,15 +221,15 @@ contract RevenueRouter is Controllable, IRevenueRouter { amounts[i] = IERC20(outAssets[i]).balanceOf(address(this)); try IPool(IAToken(outAssets[i]).POOL()).withdraw(asset, amounts[i], address(this)) { IERC20(asset).forceApprove(address(swapper), amounts[i]); - try swapper.swap(asset, stbl, amounts[i], 20_000) {} catch {} + try swapper.swap(asset, _mainToken, amounts[i], 20_000) {} catch {} } catch {} } } - uint stblGot = IERC20(stbl).balanceOf(address(this)) - stblBalanceWas; - $.units[unitIndex].pendingRevenue += stblGot; + uint mainTokenGot = IERC20(_mainToken).balanceOf(address(this)) - mainTokenBalanceWas; + $.units[unitIndex].pendingRevenue += mainTokenGot; - emit ProcessUnitRevenue(unitIndex, stblGot); + emit ProcessUnitRevenue(unitIndex, mainTokenGot); } } @@ -270,7 +288,7 @@ contract RevenueRouter is Controllable, IRevenueRouter { } uint amountToSwap = amount; - address stbl = $.stbl; + address mainToken = $.token; ISwapper swapper = ISwapper(IPlatform(platform()).swapper()); { @@ -281,17 +299,19 @@ contract RevenueRouter is Controllable, IRevenueRouter { IERC20(asset).safeTransfer(_recovery, toRecovery); address[] memory assetsToRegister = new address[](1); assetsToRegister[0] = asset; - IRecovery(_recovery).registerAssets(assetsToRegister); + IRecoveryBase(_recovery).registerAssets(assetsToRegister); } } - if (stbl != address(0)) { - uint stblBalanceWas = IERC20(stbl).balanceOf(address(this)); + if (mainToken != address(0)) { + uint mainTokenBalanceWas = IERC20(mainToken).balanceOf(address(this)); IERC20(asset).forceApprove(address(swapper), amountToSwap); - try swapper.swap(asset, stbl, amountToSwap, 20_000) { - uint stblGot = IERC20(stbl).balanceOf(address(this)) - stblBalanceWas; - uint xGot = stblGot * $.xShare / DENOMINATOR; - IERC20(stbl).safeTransfer($.feeTreasury, stblGot - xGot); + try swapper.swap(asset, mainToken, amountToSwap, 20_000) { + uint mainTokenGot = IERC20(mainToken).balanceOf(address(this)) - mainTokenBalanceWas; + uint xGot = mainTokenGot * $.xShare / DENOMINATOR; + if (mainTokenGot > xGot) { + IERC20(mainToken).safeTransfer($.feeTreasury, mainTokenGot - xGot); + } $.pendingRevenue += xGot; if (cleanup) { $.assetsAccumulated.remove(_assetsAccumulated[i]); @@ -385,9 +405,9 @@ contract RevenueRouter is Controllable, IRevenueRouter { /// @inheritdoc IRevenueRouter function addresses() external view returns (address[] memory) { RevenueRouterStorage storage $ = _getRevenueRouterStorage(); - address[] memory _addresses = new address[](4); - _addresses[0] = $.stbl; - _addresses[1] = $.xStbl; + address[] memory _addresses = new address[](COUNT_ADDRESSES); + _addresses[0] = $.token; + _addresses[1] = $.xToken; _addresses[2] = $.xStaking; _addresses[3] = $.feeTreasury; return _addresses; @@ -398,6 +418,11 @@ contract RevenueRouter is Controllable, IRevenueRouter { return _getRevenueRouterStorage().assetsAccumulated.values(); } + /// @inheritdoc IRevenueRouter + function xShare() external view returns (uint) { + return _getRevenueRouterStorage().xShare; + } + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* INTERNAL LOGIC */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ diff --git a/src/tokenomics/StabilityDAO.sol b/src/tokenomics/StabilityDAO.sol deleted file mode 100644 index 92a3c255..00000000 --- a/src/tokenomics/StabilityDAO.sol +++ /dev/null @@ -1,248 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.28; - -import {Controllable, IControllable} from "../core/base/Controllable.sol"; -import {ERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; -import { - ERC20BurnableUpgradeable -} from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20BurnableUpgradeable.sol"; -import {IStabilityDAO} from "../interfaces/IStabilityDAO.sol"; -import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; -import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; -import {ConstantsLib} from "../core/libs/ConstantsLib.sol"; - -/// @title Stability DAO Token contract -/// Amount of tokens for each user represents their voting power in the DAO. -/// Only users with high enough amount of staked xSTBL have DAO-tokens. -/// Tokens are non-transferable and can be only minted and burned by XStaking contract. -/// @author Omriss (https://github.com/omriss) -/// Changelog: -/// 1.0.1: userPower is renamed to getVotes (compatibility with OpenZeppelin's ERC20Votes) - #423 -contract StabilityDAO is - Controllable, - ERC20Upgradeable, - ERC20BurnableUpgradeable, - ReentrancyGuardUpgradeable, - IStabilityDAO -{ - /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ - /* CONSTANTS */ - /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ - - /// @inheritdoc IControllable - string public constant VERSION = "1.0.1"; - - // keccak256(abi.encode(uint(keccak256("erc7201:stability.StabilityDAO")) - 1)) & ~bytes32(uint(0xff)); - bytes32 private constant _STABILITY_DAO_TOKEN_STORAGE_LOCATION = - 0xb41400b8ab7d5c4f4647f6397fc72c137345511eb9c9a0082de7fe729c2ae200; - - /// @dev Same to XSTBL.DENOMINATOR - uint internal constant DENOMINATOR_XSTBL = 10_000; - - //region ----------------------------------- Data types - /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ - /* DATA TYPES */ - /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ - - /// @custom:storage-location erc7201:stability.StabilityDAO - struct StabilityDaoStorage { - /// @dev Mapping is used to be able to add new fields to DaoParams struct in future, only config[0] is used - mapping(uint => DaoParams) config; - /// @notice Address of XSTBL token - address xStbl; - /// @notice Address of xStaking contract - address xStaking; - /// @notice Address to which a user has delegated his vote power - mapping(address user => address) delegatedTo; - /// @notice Set of addresses that have delegated their vote power to a user - mapping(address user => EnumerableSet.AddressSet) delegators; - } - - error NonTransferable(); - error NotDelegatedTo(); - error AlreadyDelegated(); - error WrongValue(); - - event ConfigUpdated(DaoParams newConfig); - event DelegatePower(address from, address to); - event UnDelegatePower(address from, address to); - - //endregion ----------------------------------- Data types - - //region ----------------------------------- Initialization and modifiers - /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ - /* MODIFIERS */ - /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ - modifier onlyXStaking() virtual { - _requireXStaking(); - _; - } - - /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ - /* INITIALIZATION */ - /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ - - /// @inheritdoc IStabilityDAO - function initialize(address platform_, address xStbl_, address xStaking_, DaoParams memory p) public initializer { - __Controllable_init(platform_); - __ERC20_init("Stability DAO", "STBL_DAO"); - StabilityDaoStorage storage $ = _getStorage(); - $.xStaking = xStaking_; - $.xStbl = xStbl_; - $.config[0] = p; - } - - //endregion ----------------------------------- Initialization and modifiers - - //region ----------------------------------- Actions - /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ - /* RESTRICTED ACTIONS */ - /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ - - /// @inheritdoc IStabilityDAO - function mint(address account, uint amount) external onlyXStaking { - _mint(account, amount); - } - - /// @inheritdoc IStabilityDAO - function burn(address account, uint amount) external onlyXStaking { - _burn(account, amount); - } - - /// @inheritdoc IStabilityDAO - function updateConfig(DaoParams memory p) external onlyGovernanceOrMultisig { - StabilityDaoStorage storage $ = _getStorage(); - - require(p.exitPenalty < DENOMINATOR_XSTBL, WrongValue()); - require(p.quorum < ConstantsLib.DENOMINATOR && p.proposalThreshold < ConstantsLib.DENOMINATOR, WrongValue()); - - $.config[0] = p; - - emit ConfigUpdated(p); - } - - /// @inheritdoc IStabilityDAO - function setPowerDelegation(address to) external nonReentrant { - // anyone can call this function - - StabilityDaoStorage storage $ = _getStorage(); - - if (to == msg.sender || to == address(0)) { - address delegatee = $.delegatedTo[msg.sender]; - $.delegatedTo[msg.sender] = address(0); - - //slither-disable-next-line unused-return - EnumerableSet.remove($.delegators[delegatee], msg.sender); - - emit UnDelegatePower(msg.sender, to); - } else { - require($.delegatedTo[msg.sender] == address(0), AlreadyDelegated()); - $.delegatedTo[msg.sender] = to; - - //slither-disable-next-line unused-return - EnumerableSet.add($.delegators[to], msg.sender); - - emit DelegatePower(msg.sender, to); - } - } - - //endregion ----------------------------------- Actions - - //region ----------------------------------- ERC20 hooks - /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ - /* ERC20 HOOKS */ - /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ - - /// @dev The token is not transferable, only minting and burning is allowed - function _update(address from, address to, uint value) internal override { - require(from == address(0) || to == address(0), NonTransferable()); - - super._update(from, to, value); - } - - //endregion ----------------------------------- ERC20 hooks - - //region ----------------------------------- View functions - /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ - /* VIEW FUNCTIONS */ - /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ - - /// @inheritdoc IStabilityDAO - function config() public view returns (DaoParams memory) { - return _getStorage().config[0]; - } - - /// @inheritdoc IStabilityDAO - function xStbl() public view returns (address) { - return _getStorage().xStbl; - } - - /// @inheritdoc IStabilityDAO - function xStaking() public view returns (address) { - return _getStorage().xStaking; - } - - /// @inheritdoc IStabilityDAO - function minimalPower() external view returns (uint) { - return _getStorage().config[0].minimalPower; - } - - /// @inheritdoc IStabilityDAO - function exitPenalty() external view returns (uint) { - return _getStorage().config[0].exitPenalty; - } - - /// @inheritdoc IStabilityDAO - function proposalThreshold() external view returns (uint) { - return _getStorage().config[0].proposalThreshold; - } - - /// @inheritdoc IStabilityDAO - function quorum() external view returns (uint) { - return _getStorage().config[0].quorum; - } - - /// @inheritdoc IStabilityDAO - function powerAllocationDelay() external view returns (uint) { - return _getStorage().config[0].powerAllocationDelay; - } - - /// @inheritdoc IStabilityDAO - function getVotes(address user_) public view returns (uint) { - StabilityDaoStorage storage $ = _getStorage(); - uint power = $.delegatedTo[user_] == address(0) ? balanceOf(user_) : 0; - - address[] memory delegators = EnumerableSet.values($.delegators[user_]); - uint len = delegators.length; - for (uint i; i < len; ++i) { - power += balanceOf(delegators[i]); - } - - return power; - } - - /// @inheritdoc IStabilityDAO - function delegates(address user_) external view returns (address delegatedTo, address[] memory delegators) { - StabilityDaoStorage storage $ = _getStorage(); - return ($.delegatedTo[user_], EnumerableSet.values($.delegators[user_])); - } - - //endregion ----------------------------------- View functions - - //region ----------------------------------- Internal logic - /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ - /* INTERNAL LOGIC */ - /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ - - function _requireXStaking() internal view { - require(_getStorage().xStaking == msg.sender, IncorrectMsgSender()); - } - - function _getStorage() internal pure returns (StabilityDaoStorage storage $) { - //slither-disable-next-line assembly - assembly { - $.slot := _STABILITY_DAO_TOKEN_STORAGE_LOCATION - } - } - //endregion ----------------------------------- Internal logic -} diff --git a/src/tokenomics/TokenOFTAdapter.sol b/src/tokenomics/TokenOFTAdapter.sol new file mode 100755 index 00000000..ce3ed07c --- /dev/null +++ b/src/tokenomics/TokenOFTAdapter.sol @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.22; + +import {OFTAdapterUpgradeable} from "@layerzerolabs/oft-evm-upgradeable/contracts/oft/OFTAdapterUpgradeable.sol"; +import {IControllable, Controllable} from "../core/base/Controllable.sol"; +import {IPlatform} from "../interfaces/IPlatform.sol"; +import {ITokenOFTAdapter} from "../interfaces/ITokenOFTAdapter.sol"; +import {IOFTPausable} from "../interfaces/IOFTPausable.sol"; +import {OptionsBuilder} from "@layerzerolabs/oapp-evm/contracts/oapp/libs/OptionsBuilder.sol"; + +/// @notice Omnichain Fungible Token Adapter for exist main-token +/// Changelog: +/// - 1.0.1: Add buildOptions function +contract TokenOFTAdapter is Controllable, OFTAdapterUpgradeable, ITokenOFTAdapter { + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* CONSTANTS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @inheritdoc IControllable + string public constant VERSION = "1.0.1"; + + // keccak256(abi.encode(uint(keccak256("erc7201:stability.TokenOFTAdapter")) - 1)) & ~bytes32(uint(0xff)); + bytes32 internal constant TOKEN_OFT_ADAPTER_STORAGE_LOCATION = + 0xa644c5e388c18df754c7a15986d33976363be2bae99e7e86772378f965c5c200; + + /// @custom:storage-location erc7201:stability.TokenOFTAdapter + struct TokenOftAdapterStorage { + /// @notice Paused state for addresses + mapping(address => bool) paused; + } + + //region --------------------------------- Initializers and view + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* INITIALIZATION */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + constructor(address token_, address lzEndpoint_) OFTAdapterUpgradeable(token_, lzEndpoint_) { + _disableInitializers(); + } + + /// @inheritdoc ITokenOFTAdapter + function initialize(address platform_, address delegate_) public initializer { + address _owner = IPlatform(platform_).multisig(); + + __Controllable_init(platform_); + __OApp_init(delegate_ == address(0) ? _owner : delegate_); + __Ownable_init(_owner); + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* VIEW */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @inheritdoc IOFTPausable + function paused(address account_) external view returns (bool) { + return getTokenOftAdapterStorage().paused[account_]; + } + + /// @inheritdoc IOFTPausable + function buildOptions(uint128 gas_, uint128 value_) external pure returns (bytes memory) { + return OptionsBuilder.addExecutorLzReceiveOption(OptionsBuilder.newOptions(), gas_, value_); + } + + //endregion --------------------------------- Initializers and view + + //region --------------------------------- Restricted actions + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* RESTRICTED ACTIONS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @inheritdoc IOFTPausable + function setPaused(address account, bool paused_) external onlyOperator { + TokenOftAdapterStorage storage $ = getTokenOftAdapterStorage(); + $.paused[account] = paused_; + + emit Pause(account, paused_); + } + + //endregion --------------------------------- Restricted actions + + //region --------------------------------- Overrides + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* OVERRIDES */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + function _checkOwner() internal view override { + _requireMultisig(); + } + + /// @dev Paused accounts cannot send tokens + function _debit( + address from_, + uint amountLD_, + uint minAmountLD_, + uint32 dstEid_ + ) internal virtual override returns (uint amountSentLD, uint amountReceivedLD) { + _requireNotPaused(from_); + + return super._debit(from_, amountLD_, minAmountLD_, dstEid_); + } + + /// @dev Paused accounts cannot receive tokens + function _credit( + address to_, + uint amountLD_, + uint32 srcEid_ + ) internal virtual override returns (uint amountReceivedLD) { + _requireNotPaused(to_); + + return super._credit(to_, amountLD_, srcEid_); + } + + //endregion --------------------------------- Overrides + + //region --------------------------------- Internal logic + function getTokenOftAdapterStorage() internal pure returns (TokenOftAdapterStorage storage $) { + //slither-disable-next-line assembly + assembly { + $.slot := TOKEN_OFT_ADAPTER_STORAGE_LOCATION + } + } + + function _requireNotPaused(address account) internal view { + TokenOftAdapterStorage storage $ = getTokenOftAdapterStorage(); + require(!$.paused[account] && !$.paused[address(this)], Paused()); + } + + //endregion --------------------------------- Internal logic +} diff --git a/src/tokenomics/XStaking.sol b/src/tokenomics/XStaking.sol index 89fab955..fb83615a 100644 --- a/src/tokenomics/XStaking.sol +++ b/src/tokenomics/XStaking.sol @@ -6,17 +6,18 @@ import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; import {Controllable} from "../core/base/Controllable.sol"; import {IControllable} from "../interfaces/IControllable.sol"; -import {IStabilityDAO} from "../interfaces/IStabilityDAO.sol"; +import {IDAO} from "../interfaces/IDAO.sol"; import {IXStaking} from "../interfaces/IXStaking.sol"; import {IPlatform} from "../interfaces/IPlatform.sol"; -import {IXSTBL} from "../interfaces/IXSTBL.sol"; +import {IXToken} from "../interfaces/IXToken.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -/// @title Staking contract for xSTBL +/// @title Staking contract for xToken /// Inspired by VoteModule from Ramses/Shadow codebase /// @author Alien Deployer (https://github.com/a17) /// @author Jude (https://github.com/iammrjude) /// Changelog: +/// 1.1.2: renaming xSTBL => xToken, StabilityDAO => DAO, syncStabilityDAOBalances => syncDAOBalances /// 1.1.1: syncStabilityDAOBalances - only operator /// 1.1.0: Integration with STBLDAO /// 1.0.1: use SafeERC20.safeTransfer/safeTransferFrom instead of ERC20 transfer/transferFrom @@ -28,7 +29,7 @@ contract XStaking is Controllable, ReentrancyGuardUpgradeable, IXStaking { /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ /// @inheritdoc IControllable - string public constant VERSION = "1.1.1"; + string public constant VERSION = "1.1.2"; /// @notice decimal precision of 1e18 uint public constant PRECISION = 10 ** 18; @@ -44,7 +45,7 @@ contract XStaking is Controllable, ReentrancyGuardUpgradeable, IXStaking { /// @custom:storage-location erc7201:stability.XStaking struct XStakingStorage { /// @inheritdoc IXStaking - address xSTBL; + address xToken; /// @inheritdoc IXStaking uint totalSupply; /// @inheritdoc IXStaking @@ -65,17 +66,17 @@ contract XStaking is Controllable, ReentrancyGuardUpgradeable, IXStaking { mapping(address user => uint amount) balanceOf; } - error StabilityDaoNotInitialized(); + error DaoNotInitialized(); /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* INITIALIZATION */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ - function initialize(address platform_, address xSTBL_) external initializer { + function initialize(address platform_, address xToken_) external initializer { __Controllable_init(platform_); __ReentrancyGuard_init(); XStakingStorage storage $ = _getXStakingStorage(); - $.xSTBL = xSTBL_; + $.xToken = xToken_; $.duration = 30 minutes; } @@ -103,17 +104,17 @@ contract XStaking is Controllable, ReentrancyGuardUpgradeable, IXStaking { } /// @inheritdoc IXStaking - function syncStabilityDAOBalances(address[] calldata users) external onlyOperator { + function syncDAOBalances(address[] calldata users) external onlyOperator { XStakingStorage storage $ = _getXStakingStorage(); - IStabilityDAO stabilityDao = getStabilityDAO(); - require(address(stabilityDao) != address(0), StabilityDaoNotInitialized()); + IDAO dao = getDAO(); + require(address(dao) != address(0), DaoNotInitialized()); - // @dev assume here that 1 STBL_DAO = 1 staked xSTBL always - uint threshold = stabilityDao.minimalPower(); + // @dev assume here that 1 DAO-token = 1 staked xToken always + uint threshold = dao.minimalPower(); uint len = users.length; for (uint i; i < len; ++i) { - _syncUser($, stabilityDao, users[i], threshold); + _syncUser($, dao, users[i], threshold); } } @@ -126,7 +127,7 @@ contract XStaking is Controllable, ReentrancyGuardUpgradeable, IXStaking { /// @inheritdoc IXStaking function depositAll() external { - deposit(IERC20(xSTBL()).balanceOf(msg.sender)); + deposit(IERC20(xToken()).balanceOf(msg.sender)); } /// @inheritdoc IXStaking @@ -136,15 +137,15 @@ contract XStaking is Controllable, ReentrancyGuardUpgradeable, IXStaking { XStakingStorage storage $ = _getXStakingStorage(); - /// @dev transfer xSTBL in + /// @dev transfer xToken in // slither-disable-next-line unchecked-transfer - IERC20($.xSTBL).safeTransferFrom(msg.sender, address(this), amount); + IERC20($.xToken).safeTransferFrom(msg.sender, address(this), amount); /// @dev update accounting $.totalSupply += amount; $.balanceOf[msg.sender] += amount; - /// @dev sync STBLDAO balances + /// @dev sync DAO-token balances _syncDaoTokensToBalance($, msg.sender); emit Deposit(msg.sender, amount); @@ -178,11 +179,11 @@ contract XStaking is Controllable, ReentrancyGuardUpgradeable, IXStaking { /// @dev decrement from balance mapping $.balanceOf[msg.sender] -= amount; - /// @dev transfer the xSTBL to the caller + /// @dev transfer the xToken to the caller // slither-disable-next-line unchecked-transfer - IERC20($.xSTBL).safeTransfer(msg.sender, amount); + IERC20($.xToken).safeTransfer(msg.sender, amount); - /// @dev sync STBLDAO balances + /// @dev sync DAO-token balances _syncDaoTokensToBalance($, msg.sender); emit Withdraw(msg.sender, amount); @@ -195,14 +196,14 @@ contract XStaking is Controllable, ReentrancyGuardUpgradeable, IXStaking { XStakingStorage storage $ = _getXStakingStorage(); - address _xSTBL = $.xSTBL; + address _xToken = $.xToken; - /// @dev only callable by xSTBL and RevenueRouter contract - require(msg.sender == _xSTBL || msg.sender == IXSTBL(_xSTBL).revenueRouter(), IncorrectMsgSender()); + /// @dev only callable by xToken and RevenueRouter contract + require(msg.sender == _xToken || msg.sender == IXToken(_xToken).revenueRouter(), IncorrectMsgSender()); /// @dev take the STBL from a contract to the XStaking // slither-disable-next-line unchecked-transfer - IERC20(IXSTBL(_xSTBL).STBL()).safeTransferFrom(msg.sender, address(this), amount); + IERC20(IXToken(_xToken).token()).safeTransferFrom(msg.sender, address(this), amount); uint _periodFinish = $.periodFinish; uint _duration = $.duration; @@ -240,8 +241,8 @@ contract XStaking is Controllable, ReentrancyGuardUpgradeable, IXStaking { } /// @inheritdoc IXStaking - function xSTBL() public view returns (address) { - return _getXStakingStorage().xSTBL; + function xToken() public view returns (address) { + return _getXStakingStorage().xToken; } /// @inheritdoc IXStaking @@ -294,7 +295,7 @@ contract XStaking is Controllable, ReentrancyGuardUpgradeable, IXStaking { XStakingStorage storage $ = _getXStakingStorage(); uint _totalSupply = $.totalSupply; return - /// @dev if there's no staked xSTBL + /// @dev if there's no staked xToken _totalSupply == 0 /// @dev return the existing value ? $.rewardPerTokenStored @@ -326,36 +327,36 @@ contract XStaking is Controllable, ReentrancyGuardUpgradeable, IXStaking { /* INTERNAL LOGIC */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ - /// @dev Sync balance of Stability DAO token according to the current user's balance of xSTBL - /// after depositing or withdrawing xSTBL + /// @dev Sync balance of Stability DAO token according to the current user's balance of xToken + /// after depositing or withdrawing xToken function _syncDaoTokensToBalance(XStakingStorage storage $, address user_) internal { - IStabilityDAO daoToken = getStabilityDAO(); + IDAO daoToken = getDAO(); if (address(daoToken) != address(0)) { - // @dev assume here that 1 STBL_DAO = 1 staked xSTBL always + // @dev assume here that 1 STBL_DAO = 1 staked xToken always uint threshold = daoToken.minimalPower(); _syncUser($, daoToken, user_, threshold); } } - /// @dev Sync balance of Stability DAO token for a specific user according to his current power - /// @param stabilityDao Address of the STBL_DAO token + /// @dev Sync balance of DAO token for a specific user according to his current power + /// @param dao Address of the DAO token /// @param user_ Address of the user to sync - /// @param threshold Minimal amount of staked xSTBL tokens required to have STBL_DAO - function _syncUser(XStakingStorage storage $, IStabilityDAO stabilityDao, address user_, uint threshold) internal { - uint balanceStakedXStbl = $.balanceOf[user_]; + /// @param threshold Minimal amount of staked xToken tokens required to have DAO token + function _syncUser(XStakingStorage storage $, IDAO dao, address user_, uint threshold) internal { + uint balanceStakedXTokens = $.balanceOf[user_]; - /// @dev if user has too few xSTBL staked, their STBL_DAO balance will be 0 - /// @dev otherwise user should receive 1 STBL_DAO for each 1 staked xSTBL - uint toMint = balanceStakedXStbl < threshold ? 0 : balanceStakedXStbl; - uint balanceStabilityDao = IERC20(stabilityDao).balanceOf(user_); + /// @dev if user has too few xToken staked, their DAO-token balance will be 0 + /// @dev otherwise user should receive 1 DAO-token for each 1 staked xToken + uint toMint = balanceStakedXTokens < threshold ? 0 : balanceStakedXTokens; + uint balanceDaoToken = IERC20(dao).balanceOf(user_); - if (toMint > balanceStabilityDao) { + if (toMint > balanceDaoToken) { /// @dev mint the difference - stabilityDao.mint(user_, toMint - balanceStabilityDao); - } else if (balanceStabilityDao > toMint) { + dao.mint(user_, toMint - balanceDaoToken); + } else if (balanceDaoToken > toMint) { /// @dev burn the difference - stabilityDao.burn(user_, balanceStabilityDao - toMint); + dao.burn(user_, balanceDaoToken - toMint); } } @@ -386,18 +387,18 @@ contract XStaking is Controllable, ReentrancyGuardUpgradeable, IXStaking { /// @dev zero out the stored rewards $.storedRewardsPerUser[user] = 0; - address _xSTBL = $.xSTBL; - address stbl = IXSTBL(_xSTBL).STBL(); + address _xToken = $.xToken; + address mainToken = IXToken(_xToken).token(); - /// @dev approve STBL to xSTBL - IERC20(stbl).approve(_xSTBL, reward); + /// @dev approve MainToken to xToken + IERC20(mainToken).approve(_xToken, reward); /// @dev convert - IXSTBL(_xSTBL).enter(reward); + IXToken(_xToken).enter(reward); - /// @dev transfer xSTBL to the user + /// @dev transfer xToken to the user // slither-disable-next-line unchecked-transfer - IERC20(_xSTBL).safeTransfer(user, reward); + IERC20(_xToken).safeTransfer(user, reward); emit ClaimRewards(user, reward); } @@ -410,8 +411,8 @@ contract XStaking is Controllable, ReentrancyGuardUpgradeable, IXStaking { } } - function getStabilityDAO() internal view returns (IStabilityDAO) { - return IStabilityDAO(IPlatform(IControllable(address(this)).platform()).stabilityDAO()); + function getDAO() internal view returns (IDAO) { + return IDAO(IPlatform(IControllable(address(this)).platform()).stabilityDAO()); } //endregion ----------------------------------- Internal logic diff --git a/src/tokenomics/XSTBL.sol b/src/tokenomics/XToken.sol similarity index 65% rename from src/tokenomics/XSTBL.sol rename to src/tokenomics/XToken.sol index 615e6f49..b5ea215f 100644 --- a/src/tokenomics/XSTBL.sol +++ b/src/tokenomics/XToken.sol @@ -6,22 +6,25 @@ import {ERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/ import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; import {Controllable} from "../core/base/Controllable.sol"; import {IControllable} from "../interfaces/IControllable.sol"; -import {IXSTBL} from "../interfaces/IXSTBL.sol"; +import {IXToken} from "../interfaces/IXToken.sol"; import {IXStaking} from "../interfaces/IXStaking.sol"; import {IPlatform} from "../interfaces/IPlatform.sol"; import {IRevenueRouter} from "../interfaces/IRevenueRouter.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import {IStabilityDAO} from "../interfaces/IStabilityDAO.sol"; +import {IDAO} from "../interfaces/IDAO.sol"; -/// @title xSTBL token +/// @title XToken - staked version of main token (i.e. STBL) /// Inspired by xRAM/xSHADOW from Ramses/Shadow codebase /// @author Alien Deployer (https://github.com/a17) /// @author Jude (https://github.com/iammrjude) /// @author Omriss (https://github.com/omriss) /// Changelog: +/// 1.2.0: add list of bridges, sendToBridge, takeFromBridge - #424 +/// renaming XSTBL to XToken; params name and symbol were added to initialize() - #426 +/// Add setName and setSymbol functions. /// 1.1.0: add possibility to change the slashing penalty value - #406 /// 1.0.1: use SafeERC20.safeTransfer/safeTransferFrom instead of ERC20 transfer/transferFrom -contract XSTBL is Controllable, ERC20Upgradeable, IXSTBL { +contract XToken is Controllable, ERC20Upgradeable, IXToken { using EnumerableSet for EnumerableSet.AddressSet; using SafeERC20 for IERC20; @@ -30,22 +33,23 @@ contract XSTBL is Controllable, ERC20Upgradeable, IXSTBL { /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ /// @inheritdoc IControllable - string public constant VERSION = "1.1.0"; + string public constant VERSION = "1.2.0"; - /// @inheritdoc IXSTBL + /// @inheritdoc IXToken uint public constant BASIS = 10_000; /// @notice Default value for the slashing penalty (50%). It's used if slashingPenalty in storage is 0 uint public constant DEFAULT_SLASHING_PENALTY = 5_000; - /// @inheritdoc IXSTBL + /// @inheritdoc IXToken uint public constant MIN_VEST = 14 days; - /// @inheritdoc IXSTBL + /// @inheritdoc IXToken uint public constant MAX_VEST = 180 days; + /// @dev Name "erc7201:stability.XSTBL" is used historically // keccak256(abi.encode(uint256(keccak256("erc7201:stability.XSTBL")) - 1)) & ~bytes32(uint256(0xff)) - bytes32 private constant XSTBL_STORAGE_LOCATION = + bytes32 private constant XTOKEN_STORAGE_LOCATION = 0x8070df933051cfd06b1bc8a1cc21337087bed1e1452be7055e564e22eadb9e00; //region ---------------------------- Data types @@ -54,23 +58,29 @@ contract XSTBL is Controllable, ERC20Upgradeable, IXSTBL { /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ /// @custom:storage-location erc7201:stability.XSTBL - struct XstblStorage { - /// @inheritdoc IXSTBL - address STBL; - /// @inheritdoc IXSTBL + struct XTokenStorage { + /// @inheritdoc IXToken + address token; + /// @inheritdoc IXToken address xStaking; - /// @inheritdoc IXSTBL + /// @inheritdoc IXToken address revenueRouter; /// @dev stores the addresses that are exempt from transfer limitations when transferring out EnumerableSet.AddressSet exempt; /// @dev stores the addresses that are exempt from transfer limitations when transferring to them EnumerableSet.AddressSet exemptTo; - /// @inheritdoc IXSTBL + /// @inheritdoc IXToken uint pendingRebase; - /// @inheritdoc IXSTBL + /// @inheritdoc IXToken uint lastDistributedPeriod; - /// @inheritdoc IXSTBL + /// @inheritdoc IXToken mapping(address => VestPosition[]) vestInfo; + /// @dev addresses that are allowed to call transferToBridge + mapping(address => bool) bridges; + /// @dev Changed ERC20 name + string changedName; + /// @dev Changed ERC20 symbol + string changedSymbol; } //endregion ---------------------------- Data types @@ -81,14 +91,16 @@ contract XSTBL is Controllable, ERC20Upgradeable, IXSTBL { function initialize( address platform_, - address stbl_, + address token_, address xStaking_, - address revenueRouter_ + address revenueRouter_, + string memory name_, + string memory symbol_ ) external initializer { __Controllable_init(platform_); - __ERC20_init("xStability", "xSTBL"); - XstblStorage storage $ = _getXSTBLStorage(); - $.STBL = stbl_; + __ERC20_init(name_, symbol_); // i.e. "xStability", "xSTBL" + XTokenStorage storage $ = _getXTokenStorage(); + $.token = token_; $.xStaking = xStaking_; $.revenueRouter = revenueRouter_; @@ -96,6 +108,15 @@ contract XSTBL is Controllable, ERC20Upgradeable, IXSTBL { $.exemptTo.add(xStaking_); } + modifier onlyBridge() { + _onlyBridge(); + _; + } + + function _onlyBridge() internal view { + require(_getXTokenStorage().bridges[msg.sender], IncorrectMsgSender()); + } + //endregion ---------------------------- Initialization //region ---------------------------- Restricted actions @@ -103,9 +124,9 @@ contract XSTBL is Controllable, ERC20Upgradeable, IXSTBL { /* RESTRICTED ACTIONS */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ - /// @inheritdoc IXSTBL + /// @inheritdoc IXToken function rebase() external { - XstblStorage storage $ = _getXSTBLStorage(); + XTokenStorage storage $ = _getXTokenStorage(); address _revenueRouter = $.revenueRouter; @@ -122,7 +143,7 @@ contract XSTBL is Controllable, ERC20Upgradeable, IXSTBL { /// @dev if the rebase is greater than the Basis period > $.lastDistributedPeriod && _pendingRebase >= BASIS ) { - /// @dev PvP rebase notified to the XStaking contract to stream to xSTBL + /// @dev PvP rebase notified to the XStaking contract to stream to xToken /// @dev fetch the current period from voter $.lastDistributedPeriod = period; @@ -131,22 +152,22 @@ contract XSTBL is Controllable, ERC20Upgradeable, IXSTBL { address _xStaking = $.xStaking; - /// @dev approve STBL transferring to voteModule - IERC20($.STBL).approve(_xStaking, _pendingRebase); + /// @dev approve main-token transferring to voteModule + IERC20($.token).approve(_xStaking, _pendingRebase); - /// @dev notify the STBL rebase + /// @dev notify the main-token rebase IXStaking(_xStaking).notifyRewardAmount(_pendingRebase); emit Rebase(msg.sender, _pendingRebase); } } - /// @inheritdoc IXSTBL + /// @inheritdoc IXToken function setExemptionFrom(address[] calldata exemptee, bool[] calldata exempt) external onlyGovernanceOrMultisig { /// @dev ensure arrays of same length require(exemptee.length == exempt.length, IncorrectArrayLength()); - XstblStorage storage $ = _getXSTBLStorage(); + XTokenStorage storage $ = _getXTokenStorage(); EnumerableSet.AddressSet storage exemptFrom = $.exempt; /// @dev loop through all and attempt add/remove based on status @@ -158,12 +179,12 @@ contract XSTBL is Controllable, ERC20Upgradeable, IXSTBL { } } - /// @inheritdoc IXSTBL + /// @inheritdoc IXToken function setExemptionTo(address[] calldata exemptee, bool[] calldata exempt) external onlyGovernanceOrMultisig { /// @dev ensure arrays of same length require(exemptee.length == exempt.length, IncorrectArrayLength()); - XstblStorage storage $ = _getXSTBLStorage(); + XTokenStorage storage $ = _getXTokenStorage(); EnumerableSet.AddressSet storage exemptTo = $.exemptTo; /// @dev loop through all and attempt add/remove based on status @@ -175,6 +196,26 @@ contract XSTBL is Controllable, ERC20Upgradeable, IXSTBL { } } + /// @inheritdoc IXToken + function setBridge(address bridge_, bool status_) external onlyGovernanceOrMultisig { + XTokenStorage storage $ = _getXTokenStorage(); + $.bridges[bridge_] = status_; + } + + /// @inheritdoc IXToken + function setName(string calldata newName) external onlyOperator { + XTokenStorage storage $ = _getXTokenStorage(); + $.changedName = newName; + emit XTokenName(newName); + } + + /// @inheritdoc IXToken + function setSymbol(string calldata newSymbol) external onlyOperator { + XTokenStorage storage $ = _getXTokenStorage(); + $.changedSymbol = newSymbol; + emit XTokenSymbol(newSymbol); + } + //endregion ---------------------------- Restricted actions //region ---------------------------- User actions @@ -182,20 +223,20 @@ contract XSTBL is Controllable, ERC20Upgradeable, IXSTBL { /* USER ACTIONS */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ - /// @inheritdoc IXSTBL + /// @inheritdoc IXToken function enter(uint amount_) external { /// @dev ensure the amount_ is > 0 require(amount_ != 0, IncorrectZeroArgument()); /// @dev transfer from the caller to this address // slither-disable-next-line unchecked-transfer - IERC20(STBL()).safeTransferFrom(msg.sender, address(this), amount_); - /// @dev mint the xSTBL to the caller + IERC20(token()).safeTransferFrom(msg.sender, address(this), amount_); + /// @dev mint the xToken to the caller _mint(msg.sender, amount_); /// @dev emit an event for conversion emit Enter(msg.sender, amount_); } - /// @inheritdoc IXSTBL + /// @inheritdoc IXToken function exit(uint amount_) external returns (uint exitedAmount) { /// @dev cannot exit a 0 amount require(amount_ != 0, IncorrectZeroArgument()); @@ -204,17 +245,17 @@ contract XSTBL is Controllable, ERC20Upgradeable, IXSTBL { uint penalty = amount_ * SLASHING_PENALTY() / BASIS; uint exitAmount = amount_ - penalty; - /// @dev burn the xSTBL from the caller's address + /// @dev burn the xToken from the caller's address _burn(msg.sender, amount_); - XstblStorage storage $ = _getXSTBLStorage(); + XTokenStorage storage $ = _getXTokenStorage(); /// @dev store the rebase earned from the penalty $.pendingRebase += penalty; /// @dev transfer the exitAmount to the caller // slither-disable-next-line unchecked-transfer - IERC20($.STBL).safeTransfer(msg.sender, exitAmount); + IERC20($.token).safeTransfer(msg.sender, exitAmount); /// @dev emit actual exited amount emit InstantExit(msg.sender, exitAmount); @@ -222,7 +263,7 @@ contract XSTBL is Controllable, ERC20Upgradeable, IXSTBL { return exitAmount; } - /// @inheritdoc IXSTBL + /// @inheritdoc IXToken function createVest(uint amount_) external { /// @dev ensure not 0 require(amount_ != 0, IncorrectZeroArgument()); @@ -230,7 +271,7 @@ contract XSTBL is Controllable, ERC20Upgradeable, IXSTBL { /// @dev preemptive burn _burn(msg.sender, amount_); - XstblStorage storage $ = _getXSTBLStorage(); + XTokenStorage storage $ = _getXTokenStorage(); /// @dev fetch total length of vests uint vestLength = $.vestInfo[msg.sender].length; @@ -245,9 +286,9 @@ contract XSTBL is Controllable, ERC20Upgradeable, IXSTBL { emit NewVest(msg.sender, vestLength, amount_); } - /// @inheritdoc IXSTBL + /// @inheritdoc IXToken function exitVest(uint vestID_) external { - XstblStorage storage $ = _getXSTBLStorage(); + XTokenStorage storage $ = _getXTokenStorage(); VestPosition storage _vest = $.vestInfo[msg.sender][vestID_]; require(_vest.amount != 0, NO_VEST()); @@ -261,15 +302,15 @@ contract XSTBL is Controllable, ERC20Upgradeable, IXSTBL { if (block.timestamp < _start + MIN_VEST) { /// @dev case: vest has not crossed the minimum vesting threshold - /// @dev mint cancelled xSTBL back to msg.sender + /// @dev mint cancelled xToken back to msg.sender _mint(msg.sender, _amount); emit CancelVesting(msg.sender, vestID_, _amount); } else if (_vest.maxEnd <= block.timestamp) { /// @dev case: vest is complete - /// @dev send liquid STBL to msg.sender + /// @dev send liquid main-token to msg.sender // slither-disable-next-line unchecked-transfer - IERC20($.STBL).safeTransfer(msg.sender, _amount); + IERC20($.token).safeTransfer(msg.sender, _amount); emit ExitVesting(msg.sender, vestID_, _amount, _amount); } else { @@ -293,7 +334,7 @@ contract XSTBL is Controllable, ERC20Upgradeable, IXSTBL { /// @dev transfer underlying to the sender after penalties removed // slither-disable-next-line unchecked-transfer - IERC20($.STBL).safeTransfer(msg.sender, exitedAmount); + IERC20($.token).safeTransfer(msg.sender, exitedAmount); emit ExitVesting(msg.sender, vestID_, _amount, exitedAmount); } @@ -301,22 +342,57 @@ contract XSTBL is Controllable, ERC20Upgradeable, IXSTBL { //endregion ---------------------------- User actions + //region ---------------------------- Bridge actions + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* BRIDGES ACTIONS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @inheritdoc IXToken + function sendToBridge(address user_, uint amount_) external onlyBridge { + XTokenStorage storage $ = _getXTokenStorage(); + require(amount_ != 0 && user_ != address(0), IncorrectZeroArgument()); + + /// @dev burn the xToken from the caller's address + _burn(user_, amount_); + + /// @dev Send main-token back to the caller (bridge) + IERC20($.token).safeTransfer(msg.sender, amount_); + + emit SendToBridge(user_, amount_); + } + + /// @inheritdoc IXToken + function takeFromBridge(address user_, uint amount_) external onlyBridge { + require(amount_ != 0 && user_ != address(0), IncorrectZeroArgument()); + + /// @dev transfer from the bridge to this address + IERC20(token()).safeTransferFrom(msg.sender, address(this), amount_); + + /// @dev mint the xToken to the user address + _mint(user_, amount_); + + /// @dev emit an event for conversion + emit ReceivedFromBridge(user_, amount_); + } + + //endregion ---------------------------- Bridge actions + //region ---------------------------- View functions /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* VIEW FUNCTIONS */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ - /// @inheritdoc IXSTBL - function STBL() public view returns (address) { - return _getXSTBLStorage().STBL; + /// @inheritdoc IXToken + function token() public view returns (address) { + return _getXTokenStorage().token; } - /// @inheritdoc IXSTBL + /// @inheritdoc IXToken // solhint-disable-next-line func-name-mixedcase function SLASHING_PENALTY() public view returns (uint) { - IStabilityDAO stabilityDao = getStabilityDAO(); - if (address(stabilityDao) != address(0)) { - uint penalty = getStabilityDAO().exitPenalty(); + IDAO dao = getDAO(); + if (address(dao) != address(0)) { + uint penalty = getDAO().exitPenalty(); // @dev 0 penalty means that default value should be used if (penalty != 0) return penalty; @@ -325,39 +401,64 @@ contract XSTBL is Controllable, ERC20Upgradeable, IXSTBL { return DEFAULT_SLASHING_PENALTY; } - /// @inheritdoc IXSTBL + /// @inheritdoc IXToken function xStaking() external view returns (address) { - return _getXSTBLStorage().xStaking; + return _getXTokenStorage().xStaking; } - /// @inheritdoc IXSTBL + /// @inheritdoc IXToken function revenueRouter() external view returns (address) { - return _getXSTBLStorage().revenueRouter; + return _getXTokenStorage().revenueRouter; } - /// @inheritdoc IXSTBL + /// @inheritdoc IXToken function vestInfo(address user, uint vestID) external view returns (uint amount, uint start, uint maxEnd) { - XstblStorage storage $ = _getXSTBLStorage(); + XTokenStorage storage $ = _getXTokenStorage(); VestPosition memory vestPosition = $.vestInfo[user][vestID]; amount = vestPosition.amount; start = vestPosition.start; maxEnd = vestPosition.maxEnd; } - /// @inheritdoc IXSTBL + /// @inheritdoc IXToken function usersTotalVests(address who) external view returns (uint numOfVests) { - XstblStorage storage $ = _getXSTBLStorage(); + XTokenStorage storage $ = _getXTokenStorage(); return $.vestInfo[who].length; } - /// @inheritdoc IXSTBL + /// @inheritdoc IXToken function pendingRebase() external view returns (uint) { - return _getXSTBLStorage().pendingRebase; + return _getXTokenStorage().pendingRebase; } - /// @inheritdoc IXSTBL + /// @inheritdoc IXToken function lastDistributedPeriod() external view returns (uint) { - return _getXSTBLStorage().lastDistributedPeriod; + return _getXTokenStorage().lastDistributedPeriod; + } + + /// @inheritdoc IXToken + function isBridge(address bridge_) external view returns (bool) { + return _getXTokenStorage().bridges[bridge_]; + } + + /// @inheritdoc ERC20Upgradeable + function name() public view override returns (string memory) { + XTokenStorage storage $ = _getXTokenStorage(); + string memory changedName = $.changedName; + if (bytes(changedName).length != 0) { + return changedName; + } + return super.name(); + } + + /// @inheritdoc ERC20Upgradeable + function symbol() public view override returns (string memory) { + XTokenStorage storage $ = _getXTokenStorage(); + string memory changedSymbol = $.changedSymbol; + if (bytes(changedSymbol).length != 0) { + return changedSymbol; + } + return super.symbol(); } //endregion ---------------------------- View functions @@ -379,19 +480,19 @@ contract XSTBL is Controllable, ERC20Upgradeable, IXSTBL { /// @dev internal check for the transfer whitelist function _isExempted(address from_, address to_) internal view returns (bool) { - XstblStorage storage $ = _getXSTBLStorage(); + XTokenStorage storage $ = _getXTokenStorage(); return (from_ == address(0) || to_ == address(0) || $.exempt.contains(from_) || $.exemptTo.contains(to_)); } - function _getXSTBLStorage() internal pure returns (XstblStorage storage $) { + function _getXTokenStorage() internal pure returns (XTokenStorage storage $) { //slither-disable-next-line assembly assembly { - $.slot := XSTBL_STORAGE_LOCATION + $.slot := XTOKEN_STORAGE_LOCATION } } - function getStabilityDAO() internal view returns (IStabilityDAO) { - return IStabilityDAO(IPlatform(IControllable(address(this)).platform()).stabilityDAO()); + function getDAO() internal view returns (IDAO) { + return IDAO(IPlatform(IControllable(address(this)).platform()).stabilityDAO()); } //endregion ---------------------------- Hooks to override } diff --git a/src/tokenomics/XTokenBridge.sol b/src/tokenomics/XTokenBridge.sol new file mode 100644 index 00000000..b74e9085 --- /dev/null +++ b/src/tokenomics/XTokenBridge.sol @@ -0,0 +1,289 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import {IOAppComposer} from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppComposer.sol"; +import {OFTComposeMsgCodec} from "@layerzerolabs/oft-evm/contracts/libs/OFTComposeMsgCodec.sol"; +import {IControllable, Controllable} from "../core/base/Controllable.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IOFTPausable} from "../interfaces/IOFTPausable.sol"; +import {IXToken} from "../interfaces/IXToken.sol"; +import {IXTokenBridge} from "../interfaces/IXTokenBridge.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {SendParam, MessagingFee, OFTReceipt} from "@layerzerolabs/oft-evm/contracts/interfaces/IOFT.sol"; +import {MessagingReceipt} from "@layerzerolabs/oapp-evm/contracts/oapp/OAppSender.sol"; +import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; +import {OptionsBuilder} from "@layerzerolabs/oapp-evm/contracts/oapp/libs/OptionsBuilder.sol"; + +/// @notice XTokenBridge - bridge for xToken (i.e. xSTBL) using LayerZero Omnichain Fungible Token (OFT) bridge +/// Changelog: +/// - 1.0.1: Add buildOptions function +contract XTokenBridge is Controllable, IXTokenBridge, IOAppComposer, ReentrancyGuardUpgradeable { + using SafeERC20 for IERC20; + using OptionsBuilder for bytes; + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* CONSTANTS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @inheritdoc IControllable + string public constant VERSION = "1.0.1"; + + // keccak256(abi.encode(uint(keccak256("erc7201:stability.XTokenBridge")) - 1)) & ~bytes32(uint(0xff)); + bytes32 internal constant XTOKEN_BRIDGE_STORAGE_LOCATION = + 0x7331a1638fe957f8dc3395f52254374f52b3cbbdf185d4405a764a49dfb7f400; + + /// @notice LayerZero v2 Endpoint address + /// slither-disable-next-line naming-convention + address public immutable LZ_ENDPOINT; + + //region --------------------------------- Data types + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* Data types */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @custom:storage-location erc7201:stability.XTokenBridge + struct XTokenBridgeStorage { + /// @notice LayerZero Omnichain Fungible Token (OFT) bridge address + address bridge; + + /// @notice xToken address + address xToken; + + /// @notice xTokenBridge addresses for destination chains + mapping(uint32 dstEid_ => address xTokenBridge) xTokenBridges; + } + + //endregion --------------------------------- Data types + + //region --------------------------------- Initializers + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* Initializers */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + constructor(address lzEndpoint_) { + LZ_ENDPOINT = lzEndpoint_; + } + + /// @inheritdoc IXTokenBridge + function initialize(address platform_, address bridge_, address xToken_) public initializer { + __Controllable_init(platform_); + + XTokenBridgeStorage storage $ = _getStorage(); + $.bridge = bridge_; + $.xToken = xToken_; + // lzToken is zero by default + } + + //endregion --------------------------------- Initializers + + //region --------------------------------- View + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* View */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @inheritdoc IXTokenBridge + function bridge() external view returns (address) { + XTokenBridgeStorage storage $ = _getStorage(); + return $.bridge; + } + + /// @inheritdoc IXTokenBridge + function xToken() external view returns (address) { + XTokenBridgeStorage storage $ = _getStorage(); + return $.xToken; + } + + /// @inheritdoc IXTokenBridge + function xTokenBridge(uint32 dstEid_) external view returns (address) { + XTokenBridgeStorage storage $ = _getStorage(); + return $.xTokenBridges[dstEid_]; + } + + /// @inheritdoc IXTokenBridge + function quoteSend( + uint32 dstEid_, + uint amount, + bytes memory options + ) external view returns (MessagingFee memory msgFee) { + XTokenBridgeStorage storage $ = _getStorage(); + + /// @dev Receiver - address of this contract in another chain + address receiver = $.xTokenBridges[dstEid_]; + + SendParam memory sendParam = SendParam({ + dstEid: dstEid_, + to: bytes32(uint(uint160(receiver))), + amountLD: amount, + minAmountLD: amount, + extraOptions: options, + /// @dev ComposeMsg contains an address of the original user who initiated the transfer + composeMsg: abi.encode(msg.sender), + oftCmd: "" + }); + + // paying using ZRO token (Layer Zero token) is not supported + return IOFTPausable($.bridge).quoteSend(sendParam, false); + } + + /// @inheritdoc IXTokenBridge + function buildOptions( + uint128 gasLzReceive_, + uint128 valueLzReceive_, + uint16 _indexLzCompose, + uint128 gasLzCompose, + uint128 valueLzCompose_ + ) external pure returns (bytes memory) { + return OptionsBuilder.newOptions().addExecutorLzReceiveOption(gasLzReceive_, valueLzReceive_) + .addExecutorLzComposeOption(_indexLzCompose, gasLzCompose, valueLzCompose_); + } + + //endregion --------------------------------- View + + //region --------------------------------- Actions + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* Actions */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @inheritdoc IXTokenBridge + function setXTokenBridge( + uint32[] memory dstEids_, + address[] memory xTokenBridges_ + ) external onlyGovernanceOrMultisig { + XTokenBridgeStorage storage $ = _getStorage(); + uint len = dstEids_.length; + require(len == xTokenBridges_.length, IControllable.IncorrectArrayLength()); + + for (uint i; i < len; ++i) { + $.xTokenBridges[dstEids_[i]] = xTokenBridges_[i]; + } + + emit SetXTokenBridges(dstEids_, xTokenBridges_); + } + + /// @inheritdoc IXTokenBridge + function send( + uint32 dstEid_, + uint amount, + MessagingFee memory msgFee, + bytes memory options + ) external payable nonReentrant { + XTokenBridgeStorage storage $ = _getStorage(); + address _bridge = $.bridge; + + // ----------------- check amount and value + require(amount != 0, ZeroAmount()); + /// @dev exact value must be sent otherwise excess will leave stuck in the contract + require(msg.value == msgFee.nativeFee, IncorrectNativeValue()); + + // ----------------- ensure that sender is not paused (the bridge is not able to check it on its own) + require(!IOFTPausable(_bridge).paused(msg.sender), SenderPaused()); + + // ----------------- prepare main-token amount to send through the bridge + address _xToken = $.xToken; + + /// @dev main-token address (STBL) + address token = IXToken(_xToken).token(); + + { + IXToken(_xToken).sendToBridge(msg.sender, amount); + require(IERC20(token).balanceOf(address(this)) >= amount, IncorrectAmountReceivedFromXToken()); + } + + IERC20(token).forceApprove(_bridge, amount); + + // ----------------- send main-token through the bridge + /// @dev Receiver - address of this contract in another chain + address receiver = $.xTokenBridges[dstEid_]; + require(receiver != address(0), ChainNotSupported()); + + SendParam memory sendParam = SendParam({ + dstEid: dstEid_, + to: bytes32(uint(uint160(receiver))), + amountLD: amount, + minAmountLD: amount, + extraOptions: options, + /// @dev ComposeMsg contains an address of the original user who initiated the transfer + composeMsg: abi.encode(msg.sender), + oftCmd: "" + }); + + (MessagingReceipt memory r, OFTReceipt memory oftReceipt) = + IOFTPausable(_bridge).send{value: msgFee.nativeFee}(sendParam, msgFee, msg.sender); + + emit XTokenSent(msg.sender, dstEid_, amount, oftReceipt.amountSentLD, r.guid, r.nonce, r.fee.nativeFee); + } + + /// @inheritdoc IXTokenBridge + function salvage(address token, uint amount, address receiver) external onlyGovernanceOrMultisig { + if (amount == 0) { + amount = IERC20(token).balanceOf(address(this)); + } + IERC20(token).safeTransfer(receiver, amount); + } + + //endregion --------------------------------- Actions + + //region --------------------------------- IOAppComposer + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* IOAppComposer */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @notice Handles composed messages from the OFT: staking received main-token to xToken for the recipient + /// @param oApp_ Address of the originating OApp (must be trusted OFT) + /// @param guid_ Unique identifier for this message + /// @param message_ Encoded message containing compose data. + /// The message is generated inside OFT-adapter.lzReceive on destination chain. + function lzCompose( + address oApp_, + bytes32 guid_, + bytes calldata message_, + address, + /*_executor*/ + bytes calldata /*_extraData*/ + ) external payable override nonReentrant { + XTokenBridgeStorage storage $ = _getStorage(); + address _bridge = $.bridge; + + // ---------------- Verify the message source + require(msg.sender == LZ_ENDPOINT, UnauthorizedSender()); + require(oApp_ == _bridge, UntrustedOApp()); + + uint32 srcEid = OFTComposeMsgCodec.srcEid(message_); + { + bytes32 composeFromBytes = OFTComposeMsgCodec.composeFrom(message_); + /// @dev an instance of xTokenBridges which initiated the OFT transfer + address senderXTokenBridge = OFTComposeMsgCodec.bytes32ToAddress(composeFromBytes); + require($.xTokenBridges[srcEid] == senderXTokenBridge, InvalidSenderXTokenBridge()); + } + + // ---------------- Decode the message + uint amountLD = OFTComposeMsgCodec.amountLD(message_); + address recipient = abi.decode(OFTComposeMsgCodec.composeMsg(message_), (address)); + + require(recipient != address(0), IncorrectReceiver()); // just for safety + require(amountLD != 0, ZeroAmount()); // just for safety + + // ---------------- stake main-token for the user + IERC20(IXToken($.xToken).token()).forceApprove($.xToken, amountLD); + IXToken($.xToken).takeFromBridge(recipient, amountLD); + // we don't check result user balance here to reduce gas consumption + + emit Staked(recipient, srcEid, amountLD, guid_); + } + + //endregion --------------------------------- IOAppComposer + + //region --------------------------------- Internal utils + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* Internal utils */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + function _getStorage() internal pure returns (XTokenBridgeStorage storage $) { + //slither-disable-next-line assembly + assembly { + $.slot := XTOKEN_BRIDGE_STORAGE_LOCATION + } + } + + //endregion --------------------------------- Internal utils +} diff --git a/src/tokenomics/libs/RecoveryRelayerLib.sol b/src/tokenomics/libs/RecoveryRelayerLib.sol new file mode 100755 index 00000000..22122e00 --- /dev/null +++ b/src/tokenomics/libs/RecoveryRelayerLib.sol @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +library RecoveryRelayerLib { + using EnumerableSet for EnumerableSet.AddressSet; + using SafeERC20 for IERC20; + + // keccak256(abi.encode(uint(keccak256("erc7201:stability.RecoveryRelayer")) - 1)) & ~bytes32(uint(0xff)); + bytes32 internal constant _RECOVERY_RELAYER_STORAGE_LOCATION = + 0xdd1a9ce3728ddab87b43e5829ea263572add34ec16b3c991bb693f66c8715d00; + + //region -------------------------------------- Data types + + error NotWhitelisted(); + + event RegisterTokens(address[] tokens); + event SetThresholds(address[] tokens, uint[] thresholds); + event Whitelist(address operator, bool add); + + /// @custom:storage-location erc7201:stability.RecoveryRelayer + struct RecoveryRelayerStorage { + /// @notice Minimum thresholds for tokens to trigger a swap + mapping(address token => uint threshold) tokenThresholds; + /// @notice Whitelisted operators that can call main actions + mapping(address operator => bool allowed) whitelistOperators; + /// @notice All tokens with not zero amounts - possible swap sources + EnumerableSet.AddressSet registeredTokens; + } + + //endregion -------------------------------------- Data types + + //region -------------------------------------- View + /// @notice Return list of registered tokens with amounts exceeding thresholds + /// Meta vault tokens are excluded from the list + function getListTokensToSwap(RecoveryRelayerStorage storage $) external view returns (address[] memory tokens) { + uint len = $.registeredTokens.length(); + address[] memory tempTokens = new address[](len); + uint countNotZero; + for (uint i; i < len; ++i) { + address token = $.registeredTokens.at(i); + uint balance = IERC20(token).balanceOf(address(this)); + if (balance > $.tokenThresholds[token]) { + tempTokens[countNotZero] = token; + countNotZero++; + } + } + + return _removeEmpty(tempTokens, countNotZero); + } + + function getListRegisteredTokens(RecoveryRelayerStorage storage $) external view returns (address[] memory tokens) { + return $.registeredTokens.values(); + } + + //endregion -------------------------------------- View + + //region -------------------------------------- Governance actions + function setThresholds(address[] memory tokens, uint[] memory thresholds) internal { + RecoveryRelayerStorage storage $ = getRecoveryRelayerStorage(); + uint len = tokens.length; + for (uint i; i < len; ++i) { + $.tokenThresholds[tokens[i]] = thresholds[i]; + } + emit SetThresholds(tokens, thresholds); + } + + function changeWhitelist(address operator_, bool add_) internal { + RecoveryRelayerStorage storage $ = getRecoveryRelayerStorage(); + $.whitelistOperators[operator_] = add_; + + emit Whitelist(operator_, add_); + } + + //endregion -------------------------------------- Governance actions + + //region -------------------------------------- Actions + + /// @notice Register income. Select a pool with minimum price and detect its token 1. + /// Swap all {tokens} to the token1. Buy recovery tokens using token 1. + function registerAssets(address[] memory tokens_) internal { + RecoveryRelayerStorage storage $ = getRecoveryRelayerStorage(); + + emit RegisterTokens(tokens_); + uint len = tokens_.length; + for (uint i; i < len; ++i) { + $.registeredTokens.add(tokens_[i]); + } + } + + //endregion -------------------------------------- Actions + + //region -------------------------------------- Utils + + function getRecoveryRelayerStorage() internal pure returns (RecoveryRelayerStorage storage $) { + //slither-disable-next-line assembly + assembly { + $.slot := _RECOVERY_RELAYER_STORAGE_LOCATION + } + } + + /// @notice Remove zero items from the given array + function _removeEmpty(address[] memory items, uint countNotZero) internal pure returns (address[] memory dest) { + uint len = items.length; + dest = new address[](countNotZero); + + uint index = 0; + for (uint i; i < len; ++i) { + if (items[i] != address(0)) { + dest[index] = items[i]; + index++; + } + } + } + + //endregion -------------------------------------- Utils +} diff --git a/test/core/MetaVault.MaxDeposit.MetaS.Sonic.t.sol b/test/core/MetaVault.MaxDeposit.MetaS.Sonic.t.sol index 833e72df..e6701335 100644 --- a/test/core/MetaVault.MaxDeposit.MetaS.Sonic.t.sol +++ b/test/core/MetaVault.MaxDeposit.MetaS.Sonic.t.sol @@ -567,7 +567,7 @@ contract MetaVaultMaxDepositMetaSSonicTest is Test { return wrapped.balanceOf(address(this)) - balanceBefore; } - function _getEmittedConsumedAmount() internal returns (uint amountConsumedEmitted) { + function _getEmittedConsumedAmount() internal view returns (uint amountConsumedEmitted) { Vm.Log[] memory logs = vm.getRecordedLogs(); bytes32 eventSig = keccak256("DepositAssets(address,address[],uint256[],uint256)"); @@ -584,7 +584,7 @@ contract MetaVaultMaxDepositMetaSSonicTest is Test { return 0; } - function _getDepositAmountToWrapped() internal returns (uint amountConsumedEmitted) { + function _getDepositAmountToWrapped() internal view returns (uint amountConsumedEmitted) { Vm.Log[] memory logs = vm.getRecordedLogs(); bytes32 eventSig = keccak256("Deposit(address,address,uint256,uint256)"); diff --git a/test/core/MetaVault.MaxDeposit.MetaUSD.Sonic.t.sol b/test/core/MetaVault.MaxDeposit.MetaUSD.Sonic.t.sol index 8c1cec6b..09b61221 100644 --- a/test/core/MetaVault.MaxDeposit.MetaUSD.Sonic.t.sol +++ b/test/core/MetaVault.MaxDeposit.MetaUSD.Sonic.t.sol @@ -753,7 +753,7 @@ contract MetaVaultMaxDepositMetaUsdSonicTest is Test { return wrapped.balanceOf(address(this)) - balanceBefore; } - function _getEmittedConsumedAmount() internal returns (uint amountConsumedEmitted) { + function _getEmittedConsumedAmount() internal view returns (uint amountConsumedEmitted) { Vm.Log[] memory logs = vm.getRecordedLogs(); bytes32 eventSig = keccak256("DepositAssets(address,address[],uint256[],uint256)"); @@ -770,7 +770,7 @@ contract MetaVaultMaxDepositMetaUsdSonicTest is Test { return 0; } - function _getDepositAmountToWrapped() internal returns (uint amountConsumedEmitted) { + function _getDepositAmountToWrapped() internal view returns (uint amountConsumedEmitted) { Vm.Log[] memory logs = vm.getRecordedLogs(); bytes32 eventSig = keccak256("Deposit(address,address,uint256,uint256)"); diff --git a/test/periphery/PriceAggregatorOApp.t.sol b/test/periphery/PriceAggregatorOApp.t.sol new file mode 100644 index 00000000..a78d557f --- /dev/null +++ b/test/periphery/PriceAggregatorOApp.t.sol @@ -0,0 +1,561 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {console, Test} from "forge-std/Test.sol"; +import {IPlatform} from "../../src/interfaces/IPlatform.sol"; +import {IControllable} from "../../src/interfaces/IControllable.sol"; +import {IBridgedPriceOracle} from "../../src/interfaces/IBridgedPriceOracle.sol"; +import {IPriceAggregator} from "../../src/interfaces/IPriceAggregator.sol"; +import {IPriceAggregatorOApp} from "../../src/interfaces/IPriceAggregatorOApp.sol"; +import {SonicConstantsLib} from "../../chains/sonic/SonicConstantsLib.sol"; +import {Proxy} from "../../src/core/proxy/Proxy.sol"; +import {AvalancheConstantsLib} from "../../chains/avalanche/AvalancheConstantsLib.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {MessagingFee} from "@layerzerolabs/oft-evm/contracts/interfaces/IOFT.sol"; +import {Origin} from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppReceiver.sol"; +// import {OFTMsgCodec} from "@layerzerolabs/oft-evm/contracts/libs/OFTMsgCodec.sol"; +import {OptionsBuilder} from "@layerzerolabs/oapp-evm/contracts/oapp/libs/OptionsBuilder.sol"; +// import {InboundPacket, PacketDecoder} from "@layerzerolabs/lz-evm-protocol-v2/../oapp/contracts/precrime/libs/Packet.sol"; +import {PacketV1Codec} from "@layerzerolabs/lz-evm-protocol-v2/contracts/messagelib/libs/PacketV1Codec.sol"; +import {IOAppReceiver} from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppReceiver.sol"; +import {PriceAggregatorOApp} from "../../src/periphery/PriceAggregatorOApp.sol"; +import {BridgedPriceOracle} from "../../src/periphery/BridgedPriceOracle.sol"; +import {BridgeTestLib} from "../tokenomics/libs/BridgeTestLib.sol"; + +contract PriceAggregatorOAppTest is Test { + using PacketV1Codec for bytes; + using SafeERC20 for IERC20; + + uint private constant SONIC_FORK_BLOCK = 52228979; // Oct-28-2025 01:14:21 PM +UTC + uint private constant AVALANCHE_FORK_BLOCK = 71037861; // Oct-28-2025 13:17:17 UTC + uint private constant PLASMA_FORK_BLOCK = 5398928; // Nov-5-2025 07:38:59 UTC + + /// @dev Gas limit for executor lzReceive calls + /// 2 mln => fee = 0.78 S + /// 100_000 => fee = 0.36 S + uint128 private constant GAS_LIMIT = 30_000; + + PriceAggregatorOApp internal priceAggregatorOApp; + BridgedPriceOracle internal bridgedPriceOracleAvalanche; + BridgedPriceOracle internal bridgedPriceOraclePlasma; + + BridgeTestLib.ChainConfig internal sonic; + BridgeTestLib.ChainConfig internal avalanche; + BridgeTestLib.ChainConfig internal plasma; + + address internal constant TEST_DELEGATOR = address(0x999); + + constructor() { + { + uint forkSonic = vm.createFork(vm.envString("SONIC_RPC_URL"), SONIC_FORK_BLOCK); + uint forkAvalanche = vm.createFork(vm.envString("AVALANCHE_RPC_URL"), AVALANCHE_FORK_BLOCK); + uint forkPlasma = vm.createFork(vm.envString("PLASMA_RPC_URL"), PLASMA_FORK_BLOCK); + + sonic = BridgeTestLib.createConfigSonic(vm, forkSonic, TEST_DELEGATOR); + avalanche = BridgeTestLib.createConfigAvalanche(vm, forkAvalanche, TEST_DELEGATOR); + plasma = BridgeTestLib.createConfigPlasma(vm, forkPlasma, TEST_DELEGATOR); + } + + // ------------------- Create adapter and bridged token + priceAggregatorOApp = PriceAggregatorOApp(setupPriceAggregatorOAppOnSonic(TEST_DELEGATOR)); + bridgedPriceOracleAvalanche = BridgedPriceOracle(setupBridgedPriceOracle(avalanche, TEST_DELEGATOR)); + bridgedPriceOraclePlasma = BridgedPriceOracle(setupBridgedPriceOracle(plasma, TEST_DELEGATOR)); + + sonic.oapp = address(priceAggregatorOApp); + avalanche.oapp = address(bridgedPriceOracleAvalanche); + plasma.oapp = address(bridgedPriceOraclePlasma); + + // ------------------- Set up Sonic:Avalanche + BridgeTestLib.setUpSonicAvalanche(vm, sonic, avalanche); + + // ------------------- Set up Sonic:Plasma + BridgeTestLib.setUpSonicPlasma(vm, sonic, plasma); + } + + //region ------------------------------------- Unit tests for PriceAggregatorOApp + function testViewPriceAggregatorOApp() public { + vm.selectFork(sonic.fork); + + console.log("erc7201:stability.PriceAggregatorOApp"); + console.logBytes32( + keccak256(abi.encode(uint(keccak256("erc7201:stability.PriceAggregatorOApp")) - 1)) & ~bytes32(uint(0xff)) + ); + + assertEq(priceAggregatorOApp.entity(), SonicConstantsLib.TOKEN_STBL, "stbl"); + assertEq(priceAggregatorOApp.platform(), SonicConstantsLib.PLATFORM, "PriceAggregatorOApp - platform"); + assertEq(priceAggregatorOApp.owner(), sonic.multisig, "PriceAggregatorOApp - owner"); + } + + function testWhitelist() public { + vm.selectFork(sonic.fork); + + vm.prank(address(this)); + vm.expectRevert(IControllable.NotGovernanceAndNotMultisig.selector); + priceAggregatorOApp.changeWhitelist(address(this), true); + + vm.prank(sonic.multisig); + priceAggregatorOApp.changeWhitelist(address(this), true); + + bool isWhitelisted = priceAggregatorOApp.isWhitelisted(address(this)); + assertEq(isWhitelisted, true, "is whitelisted"); + + vm.prank(sonic.multisig); + priceAggregatorOApp.changeWhitelist(address(this), false); + + isWhitelisted = priceAggregatorOApp.isWhitelisted(address(this)); + assertEq(isWhitelisted, false, "not whitelisted"); + } + + function testPriceAggregatorOAppSetPeers() public { + vm.selectFork(sonic.fork); + + vm.prank(address(this)); + vm.expectRevert(); + priceAggregatorOApp.setPeer(avalanche.endpointId, bytes32(uint(uint160(address(bridgedPriceOracleAvalanche))))); + + vm.prank(sonic.multisig); + priceAggregatorOApp.setPeer(avalanche.endpointId, bytes32(uint(uint160(address(bridgedPriceOracleAvalanche))))); + + assertEq( + priceAggregatorOApp.peers(avalanche.endpointId), + bytes32(uint(uint160(address(bridgedPriceOracleAvalanche)))) + ); + } + + function testLzReceiveUnsupported() public { + vm.selectFork(sonic.fork); + + Origin memory origin = Origin({ + srcEid: AvalancheConstantsLib.LAYER_ZERO_V2_ENDPOINT_ID, + sender: bytes32(uint(uint160(address(bridgedPriceOracleAvalanche)))), + nonce: 1 + }); + + vm.prank(sonic.endpoint); + vm.expectRevert(IPriceAggregatorOApp.UnsupportedOperation.selector); + priceAggregatorOApp.lzReceive( + origin, + bytes32(0), // guid: actual value doesn't matter + hex"00", // empty message + address(0), // executor + "" // extraData + ); + } + + //endregion ------------------------------------- Unit tests for PriceAggregatorOApp + + //region ------------------------------------- Unit tests for BridgedPriceOracleAvalanche + function testViewBridgedPriceOracle() public { + vm.selectFork(avalanche.fork); + + // console.log("erc7201:stability.BridgedPriceOracle"); + // console.logBytes32( + // keccak256(abi.encode(uint(keccak256("erc7201:stability.BridgedPriceOracle")) - 1)) & ~bytes32(uint(0xff)) + // ); + + assertEq(bridgedPriceOracleAvalanche.decimals(), 8, "decimals in aave price oracle is 8"); + assertEq( + bridgedPriceOracleAvalanche.platform(), AvalancheConstantsLib.PLATFORM, "bridgedPriceOracle - platform" + ); + assertEq(bridgedPriceOracleAvalanche.owner(), avalanche.multisig, "bridgedPriceOracle - owner"); + assertEq(bridgedPriceOracleAvalanche.tokenSymbol(), "STBL", "token symbol is correct"); + } + + function testBridgedPriceOraclePeers() public { + vm.selectFork(avalanche.fork); + + vm.prank(address(this)); + vm.expectRevert(); + bridgedPriceOracleAvalanche.setPeer(sonic.endpointId, bytes32(uint(uint160(address(priceAggregatorOApp))))); + + vm.prank(avalanche.multisig); + bridgedPriceOracleAvalanche.setPeer(sonic.endpointId, bytes32(uint(uint160(address(priceAggregatorOApp))))); + + assertEq( + bridgedPriceOracleAvalanche.peers(sonic.endpointId), bytes32(uint(uint160(address(priceAggregatorOApp)))) + ); + } + + function testInvalidMessageFormat() public { + vm.selectFork(avalanche.fork); + + Origin memory origin = + Origin({srcEid: sonic.endpointId, sender: bytes32(uint(uint160(address(priceAggregatorOApp)))), nonce: 1}); + + // same code as OAppEncodingLib.packPriceUsd18 + bytes32 brokenSerializedMessage = bytes32( + (uint(222) << 240) // (!) incorrect message format + | (uint(uint160(1)) << 80) | (uint(uint64(2)) << 16) + ); + + vm.expectRevert(IBridgedPriceOracle.InvalidMessageFormat.selector); + vm.prank(avalanche.endpoint); + IOAppReceiver(avalanche.oapp) + .lzReceive( + origin, + bytes32(0), // guid: actual value doesn't matter + abi.encodePacked(brokenSerializedMessage), + address(0), // executor + "" // extraData + ); + } + + //endregion ------------------------------------- Unit tests for BridgedPriceOracleAvalanche + + //region ------------------------------------- Send price from Sonic to Avalanche + function testSendPriceToAvalanche() public { + _testSendPriceToDest(avalanche); + } + + function testSendPriceToPlasma() public { + _testSendPriceToDest(plasma); + } + + function testSendPriceToAvalancheBadPaths() public { + vm.selectFork(sonic.fork); + + address sender = address(0x1); + deal(sender, 2 ether); // to pay fees + + (uint priceSonic,) = _setPriceOnSonic(1.7e18); + + // ------------------- Send price to Avalanche + vm.selectFork(sonic.fork); + + bytes memory options = OptionsBuilder.addExecutorLzReceiveOption(OptionsBuilder.newOptions(), 2_000_000, 0); + MessagingFee memory msgFee = + priceAggregatorOApp.quotePriceMessage(AvalancheConstantsLib.LAYER_ZERO_V2_ENDPOINT_ID, options, false); + + vm.recordLogs(); + + // ------------------- Not whitelisted (!) + vm.prank(sender); + vm.expectRevert(IPriceAggregatorOApp.NotWhitelisted.selector); + priceAggregatorOApp.sendPriceMessage{value: msgFee.nativeFee}( + AvalancheConstantsLib.LAYER_ZERO_V2_ENDPOINT_ID, options, msgFee + ); + + // ------------------- Whitelisted + vm.selectFork(sonic.fork); + vm.prank(sonic.multisig); + priceAggregatorOApp.changeWhitelist(sender, true); + + vm.prank(sender); + priceAggregatorOApp.sendPriceMessage{value: msgFee.nativeFee}( + AvalancheConstantsLib.LAYER_ZERO_V2_ENDPOINT_ID, options, msgFee + ); + (bytes memory message,) = BridgeTestLib._extractSendMessage(vm.getRecordedLogs()); + + // ------------------ Avalanche: simulate message reception + vm.selectFork(avalanche.fork); + + Origin memory origin = + Origin({srcEid: sonic.endpointId, sender: bytes32(uint(uint160(address(priceAggregatorOApp)))), nonce: 1}); + + vm.prank(avalanche.endpoint); + bridgedPriceOracleAvalanche.lzReceive( + origin, + bytes32(0), // guid: actual value doesn't matter + message, + address(0), // executor + "" // extraData + ); + + (uint priceAvalanche,) = _sendPriceFromSonicToDest(sender, avalanche); + assertEq(priceSonic, 1.7e18, "new price set on Sonic"); + assertEq(priceAvalanche, 1.7e18, "new price set on Avalanche"); + } + + function testSendPriceNotTrustedSender() public { + vm.selectFork(sonic.fork); + + address sender = address(0x1); + deal(sender, 2 ether); // to pay fees + + _setPriceOnSonic(1.7e18); + + // ------------------- Send price to Avalanche + vm.selectFork(sonic.fork); + + bytes memory options = OptionsBuilder.addExecutorLzReceiveOption(OptionsBuilder.newOptions(), 2_000_000, 0); + MessagingFee memory msgFee = + priceAggregatorOApp.quotePriceMessage(AvalancheConstantsLib.LAYER_ZERO_V2_ENDPOINT_ID, options, false); + + vm.recordLogs(); + + // ------------------- Whitelisted + vm.selectFork(sonic.fork); + vm.prank(sonic.multisig); + priceAggregatorOApp.changeWhitelist(sender, true); + + vm.prank(sender); + priceAggregatorOApp.sendPriceMessage{value: msgFee.nativeFee}( + AvalancheConstantsLib.LAYER_ZERO_V2_ENDPOINT_ID, options, msgFee + ); + (bytes memory message,) = BridgeTestLib._extractSendMessage(vm.getRecordedLogs()); + + // ------------------ Avalanche: simulate message reception + vm.selectFork(avalanche.fork); + + vm.expectRevert(); // onlyPeer + vm.prank(avalanche.endpoint); + bridgedPriceOracleAvalanche.lzReceive( + Origin({ + srcEid: sonic.endpointId, + sender: bytes32(uint(uint160(address(makeAddr("not trusted sender"))))), + nonce: 1 + }), + bytes32(0), // guid: actual value doesn't matter + message, + address(0), // executor + "" // extraData + ); + } + + /// @notice Simulate situation with delayed (and so outdated) message delivery + function testOutdatedPrice() public { + uint priceBase = 1.7e18; + + // ------------------- Setup whitelist and trusted sender + vm.selectFork(sonic.fork); + + address sender = address(0x1); + deal(sender, 10 ether); // to pay fees + + vm.prank(sonic.multisig); + priceAggregatorOApp.changeWhitelist(sender, true); + + vm.selectFork(plasma.fork); + + // ------------------- Check initial price on Sonic + vm.selectFork(plasma.fork); + + // ------------------- Send basePrice to target chain + vm.selectFork(sonic.fork); + bytes memory message1; + uint timestamp1; + { + (, timestamp1) = _setPriceOnSonic(priceBase); + + bytes memory options = OptionsBuilder.addExecutorLzReceiveOption(OptionsBuilder.newOptions(), GAS_LIMIT, 0); + MessagingFee memory msgFee = priceAggregatorOApp.quotePriceMessage(plasma.endpointId, options, false); + + vm.recordLogs(); + + vm.prank(sender); + priceAggregatorOApp.sendPriceMessage{value: msgFee.nativeFee}(plasma.endpointId, options, msgFee); + (message1,) = BridgeTestLib._extractSendMessage(vm.getRecordedLogs()); + } + + skip(1 minutes); + + // ------------------- Send basePrice x 2 to target chain + bytes memory message2; + uint timestamp2; + { + (, timestamp2) = _setPriceOnSonic(2 * priceBase); + + bytes memory options = OptionsBuilder.addExecutorLzReceiveOption(OptionsBuilder.newOptions(), GAS_LIMIT, 0); + MessagingFee memory msgFee = priceAggregatorOApp.quotePriceMessage(plasma.endpointId, options, false); + + vm.recordLogs(); + + vm.prank(sender); + priceAggregatorOApp.sendPriceMessage{value: msgFee.nativeFee}(plasma.endpointId, options, msgFee); + (message2,) = BridgeTestLib._extractSendMessage(vm.getRecordedLogs()); + } + assertNotEq(timestamp1, timestamp2, "timestamps for two messages are different"); + + // ------------------ Target chain: simulate messages reception + vm.selectFork(plasma.fork); + + Origin memory origin = + Origin({srcEid: sonic.endpointId, sender: bytes32(uint(uint160(address(priceAggregatorOApp)))), nonce: 1}); + + // ------------------ At first receive Message 2 + { + vm.prank(plasma.endpoint); + IOAppReceiver(plasma.oapp) + .lzReceive( + origin, + bytes32(0), // guid: actual value doesn't matter + message2, + address(0), // executor + "" // extraData + ); + + (uint currentPrice, uint currentTimestamp) = IBridgedPriceOracle(plasma.oapp).getPriceUsd18(); + assertEq(currentPrice, 2 * priceBase, "current price after message 2"); + assertEq(currentTimestamp, timestamp2, "current timestamp after message 2"); + } + + // ------------------ Then receive outdated Message 1 + { + vm.prank(plasma.endpoint); + IOAppReceiver(plasma.oapp) + .lzReceive( + origin, + bytes32(0), // guid: actual value doesn't matter + message1, + address(0), // executor + "" // extraData + ); + + (uint currentPrice, uint currentTimestamp) = IBridgedPriceOracle(plasma.oapp).getPriceUsd18(); + assertEq(currentPrice, 2 * priceBase, "current price is not changed (it's still from message 2)"); + assertEq(currentTimestamp, timestamp2, "current timestamp wasn't changed"); + } + } + + //endregion ------------------------------------- Send price from Sonic to Avalanche + + //region ------------------------------------- Tests implementation + function _testSendPriceToDest(BridgeTestLib.ChainConfig memory dest) public { + // ------------------- Setup whitelist and trusted sender + vm.selectFork(sonic.fork); + + address sender = address(0x1); + deal(sender, 10 ether); // to pay fees + + vm.prank(sonic.multisig); + priceAggregatorOApp.changeWhitelist(sender, true); + + vm.selectFork(dest.fork); + + // ------------------- Check initial price on Sonic + vm.selectFork(dest.fork); + (uint priceBefore,) = IBridgedPriceOracle(dest.oapp).getPriceUsd18(); + assertEq(priceBefore, 0, "initial price is not set"); + + // ------------------- Set price in PriceAggregator on Sonic + (uint priceSonic, uint timestampPriceSonic) = _setPriceOnSonic(1.7e18); + + // ------------------- Send price to target chain + (uint priceAvalanche, uint timestampAvalanche) = _sendPriceFromSonicToDest(sender, dest); + + assertEq(priceSonic, 1.7e18, "price set on Sonic"); + assertEq(priceAvalanche, 1.7e18, "price set on target chain"); + assertEq(timestampAvalanche, timestampPriceSonic, "timestamp after matches timestamp sent"); + + { + int price8 = IBridgedPriceOracle(dest.oapp).latestAnswer(); + assertEq(price8, 1.7e8, "price with 8 decimals"); + } + + // ------------------- Set TINY price in PriceAggregator on Sonic + (priceSonic, timestampPriceSonic) = _setPriceOnSonic(1); + + // ------------------- Send new price to target chain + (priceAvalanche, timestampAvalanche) = _sendPriceFromSonicToDest(sender, dest); + + assertEq(priceSonic, 1, "price set on Sonic"); + assertEq(priceAvalanche, 1, "price set on target chain"); + assertEq(timestampAvalanche, timestampPriceSonic, "timestamp after matches timestamp sent"); + + // ------------------- Set HUGE price in PriceAggregator on Sonic + (priceSonic, timestampPriceSonic) = _setPriceOnSonic(17e38); + + // ------------------- Send new price to target chain + (priceAvalanche, timestampAvalanche) = _sendPriceFromSonicToDest(sender, dest); + + assertEq(priceSonic, 17e38, "price set on Sonic"); + assertEq(priceAvalanche, 17e38, "price set on target chain"); + assertEq(timestampAvalanche, timestampPriceSonic, "timestamp after matches timestamp sent"); + } + + //endregion ------------------------------------- Tests implementation + + //region ------------------------------------- Internal logic + function _setPriceOnSonic(uint targetPrice_) internal returns (uint price, uint timestamp) { + vm.selectFork(sonic.fork); + IPriceAggregator priceAggregator = IPriceAggregator(IPlatform(SonicConstantsLib.PLATFORM).priceAggregator()); + + vm.prank(sonic.multisig); + priceAggregator.addAsset(SonicConstantsLib.TOKEN_STBL, 1, 1); + + vm.prank(sonic.multisig); + priceAggregator.setMinQuorum(1); + + (,, uint roundId) = priceAggregator.price(SonicConstantsLib.TOKEN_STBL); + + address[] memory validators = priceAggregator.validators(); + + vm.prank(validators[0]); + priceAggregator.submitPrice(SonicConstantsLib.TOKEN_STBL, targetPrice_, roundId == 0 ? 1 : roundId); + + (price, timestamp,) = priceAggregator.price(SonicConstantsLib.TOKEN_STBL); + assertEq(price, targetPrice_, "expected price in price aggregator"); + } + + function _sendPriceFromSonicToDest( + address sender, + BridgeTestLib.ChainConfig memory dest + ) internal returns (uint price, uint timestamp) { + vm.selectFork(sonic.fork); + + // ------------------- Send a message with new price to target chain + bytes memory options = OptionsBuilder.addExecutorLzReceiveOption(OptionsBuilder.newOptions(), GAS_LIMIT, 0); + + MessagingFee memory msgFee = priceAggregatorOApp.quotePriceMessage(dest.endpointId, options, false); + + vm.recordLogs(); + + vm.prank(sender); + priceAggregatorOApp.sendPriceMessage{value: msgFee.nativeFee}(dest.endpointId, options, msgFee); + (bytes memory message,) = BridgeTestLib._extractSendMessage(vm.getRecordedLogs()); + + // ------------------ Target chain: simulate message reception + vm.selectFork(dest.fork); + + Origin memory origin = + Origin({srcEid: sonic.endpointId, sender: bytes32(uint(uint160(address(priceAggregatorOApp)))), nonce: 1}); + + uint gas = gasleft(); + vm.prank(dest.endpoint); + IOAppReceiver(dest.oapp) + .lzReceive( + origin, + bytes32(0), // guid: actual value doesn't matter + message, + address(0), // executor + "" // extraData + ); + uint gasUsed = gas - gasleft(); + assertLt(gasUsed, GAS_LIMIT, "gas used in lzReceive"); // ~ 30 ths + console.log("gas limit, used, fee", GAS_LIMIT, gasUsed, msgFee.nativeFee); + + (price, timestamp) = IBridgedPriceOracle(dest.oapp).getPriceUsd18(); + } + + function setupPriceAggregatorOAppOnSonic(address delegator) internal returns (address) { + vm.selectFork(sonic.fork); + + Proxy proxy = new Proxy(); + proxy.initProxy(address(new PriceAggregatorOApp(sonic.endpoint))); + PriceAggregatorOApp _PriceAggregatorOApp = PriceAggregatorOApp(address(proxy)); + _PriceAggregatorOApp.initialize(sonic.platform, SonicConstantsLib.TOKEN_STBL, delegator); + + assertEq(_PriceAggregatorOApp.owner(), sonic.multisig, "multisigSonic is owner"); + + return address(_PriceAggregatorOApp); + } + + function setupBridgedPriceOracle( + BridgeTestLib.ChainConfig memory chain, + address delegator + ) internal returns (address) { + vm.selectFork(chain.fork); + + Proxy proxy = new Proxy(); + proxy.initProxy(address(new BridgedPriceOracle(chain.endpoint))); + BridgedPriceOracle _bridgedPriceOracle = BridgedPriceOracle(address(proxy)); + _bridgedPriceOracle.initialize(address(chain.platform), "STBL", delegator); + + assertEq(_bridgedPriceOracle.owner(), chain.multisig, "multisig is owner"); + + return address(_bridgedPriceOracle); + } + + //endregion ------------------------------------- Internal logic +} diff --git a/test/periphery/libs/QAppEncodingLib.t.sol b/test/periphery/libs/QAppEncodingLib.t.sol new file mode 100644 index 00000000..52619e91 --- /dev/null +++ b/test/periphery/libs/QAppEncodingLib.t.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.22; +import {Test} from "forge-std/Test.sol"; +import {OAppEncodingLib} from "../../../src/periphery/libs/OAppEncodingLib.sol"; + +contract OAppEncodingLibWrapper { + // внешний метод принимает bytes calldata и просто вызывает реализацию библиотеки + function unpackPriceUsd18Ext(bytes calldata message) + external + pure + returns (uint16 format, uint160 price, uint64 timestamp) + { + return OAppEncodingLib.unpackPriceUsd18(message); + } +} + +contract OAppEncodingLibTest is Test { + function testPackUnpack_price1() public { + uint price = 1; + uint timestamp = 1761918746; + bytes memory message = OAppEncodingLib.packPriceUsd18(price, timestamp); + + assertEq(message.length, 32); + OAppEncodingLibWrapper wrapper = new OAppEncodingLibWrapper(); + (uint16 format, uint160 priceOut, uint64 tsOut) = wrapper.unpackPriceUsd18Ext(message); + + assertEq(uint(format), 1); + assertEq(uint(priceOut), price); + assertEq(uint64(tsOut), uint64(timestamp)); + } + + function testPackUnpack_price5999e15() public { + uint price = 5999000000000000000; // 5.999e18 + uint timestamp = 33318827546; + bytes memory message = OAppEncodingLib.packPriceUsd18(price, timestamp); + + assertEq(message.length, 32); + OAppEncodingLibWrapper wrapper = new OAppEncodingLibWrapper(); + (uint16 format, uint160 priceOut, uint64 tsOut) = wrapper.unpackPriceUsd18Ext(message); + + assertEq(uint(format), 1); + assertEq(uint(priceOut), price); + assertEq(uint64(tsOut), uint64(timestamp)); + } + + function testPackUnpack_price1234e33() public { + uint price = 1234 * 10 ** 33; // 1.234e36 + uint timestamp = 1670000002; + bytes memory message = OAppEncodingLib.packPriceUsd18(price, timestamp); + + assertEq(message.length, 32); + OAppEncodingLibWrapper wrapper = new OAppEncodingLibWrapper(); + (uint16 format, uint160 priceOut, uint64 tsOut) = wrapper.unpackPriceUsd18Ext(message); + + assertEq(uint(format), 1); + assertEq(uint(priceOut), price); + assertEq(uint64(tsOut), uint64(timestamp)); + } +} diff --git a/test/strategies/ALMF.Plasma.Upgrade.431.t.sol b/test/strategies/ALMF.Plasma.Upgrade.431.t.sol index 8c314182..41da515a 100644 --- a/test/strategies/ALMF.Plasma.Upgrade.431.t.sol +++ b/test/strategies/ALMF.Plasma.Upgrade.431.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.23; -import {Test, Vm, console} from "forge-std/Test.sol"; +import {Test, Vm} from "forge-std/Test.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {AaveLeverageMerklFarmStrategy} from "../../src/strategies/AaveLeverageMerklFarmStrategy.sol"; import {IStrategy} from "../../src/interfaces/IStrategy.sol"; @@ -12,7 +12,6 @@ import {IFactory} from "../../src/interfaces/IFactory.sol"; import {IFarmingStrategy} from "../../src/interfaces/IFarmingStrategy.sol"; import {PlasmaConstantsLib} from "../../chains/plasma/PlasmaConstantsLib.sol"; import {StrategyIdLib} from "../../src/strategies/libs/StrategyIdLib.sol"; -import {AmmAdapterIdLib} from "../../src/adapters/libs/AmmAdapterIdLib.sol"; import {ALMFLib} from "../../src/strategies/libs/ALMFLib.sol"; /// @notice Add revenueBaseAssetIndex param to farm, reset share price diff --git a/test/tokenomics/BridgedToken.t.sol b/test/tokenomics/BridgedToken.t.sol new file mode 100644 index 00000000..e6acd197 --- /dev/null +++ b/test/tokenomics/BridgedToken.t.sol @@ -0,0 +1,961 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {AvalancheConstantsLib} from "../../chains/avalanche/AvalancheConstantsLib.sol"; +import {BridgeTestLib} from "./libs/BridgeTestLib.sol"; +import {BridgedToken} from "../../src/tokenomics/BridgedToken.sol"; +import {IControllable} from "../../src/interfaces/IControllable.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IOAppReceiver} from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppReceiver.sol"; +import {IOFTPausable} from "../../src/interfaces/IOFTPausable.sol"; +import {IOFT} from "@layerzerolabs/oft-evm/contracts/interfaces/IOFT.sol"; +import {MessagingFee} from "@layerzerolabs/oft-evm/contracts/interfaces/IOFT.sol"; +import {OptionsBuilder} from "@layerzerolabs/oapp-evm/contracts/oapp/libs/OptionsBuilder.sol"; +import {Origin} from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppReceiver.sol"; +import {PacketV1Codec} from "@layerzerolabs/lz-evm-protocol-v2/contracts/messagelib/libs/PacketV1Codec.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +// import {OFTMsgCodec} from "@layerzerolabs/oft-evm/contracts/libs/OFTMsgCodec.sol"; +import {SendParam} from "@layerzerolabs/oft-evm/contracts/interfaces/IOFT.sol"; +// import {InboundPacket, PacketDecoder} from "@layerzerolabs/lz-evm-protocol-v2/../oapp/contracts/precrime/libs/Packet.sol"; +import {SonicConstantsLib} from "../../chains/sonic/SonicConstantsLib.sol"; +import {TokenOFTAdapter} from "../../src/tokenomics/TokenOFTAdapter.sol"; +import {console, Test} from "forge-std/Test.sol"; + +contract BridgedTokenTest is Test { + using OptionsBuilder for bytes; + using PacketV1Codec for bytes; + using SafeERC20 for IERC20; + + //region ------------------------------------- Constants, data types, variables + uint private constant SONIC_FORK_BLOCK = 52228979; // Oct-28-2025 01:14:21 PM +UTC + uint private constant AVALANCHE_FORK_BLOCK = 71037861; // Oct-28-2025 13:17:17 UTC + uint private constant PLASMA_FORK_BLOCK = 5398928; // Nov-5-2025 07:38:59 UTC + + /// @dev Gas limit for executor lzReceive calls + /// 2 mln => fee = 0.78 S + /// 100_000 => fee = 0.36 S + uint128 private constant GAS_LIMIT = 65_000; + + TokenOFTAdapter internal adapter; + BridgedToken internal bridgedTokenAvalanche; + BridgedToken internal bridgedTokenPlasma; + + address private constant TEST_DELEGATOR = address(0x9999); + + struct ChainResults { + uint balanceSenderMainToken; + uint balanceContractMainToken; + uint balanceReceiverMainToken; + uint totalSupplyMainToken; + uint balanceSenderEther; + } + + struct Results { + ChainResults srcBefore; + ChainResults targetBefore; + ChainResults srcAfter; + ChainResults targetAfter; + uint nativeFee; + } + + struct TestCaseSendToTarget { + address sender; + uint sendAmount; + uint initialBalance; + address receiver; + } + + BridgeTestLib.ChainConfig internal sonic; + BridgeTestLib.ChainConfig internal avalanche; + BridgeTestLib.ChainConfig internal plasma; + //endregion ------------------------------------- Constants, data types, variables + + constructor() { + { + uint forkSonic = vm.createFork(vm.envString("SONIC_RPC_URL"), SONIC_FORK_BLOCK); + uint forkAvalanche = vm.createFork(vm.envString("AVALANCHE_RPC_URL"), AVALANCHE_FORK_BLOCK); + uint forkPlasma = vm.createFork(vm.envString("PLASMA_RPC_URL"), PLASMA_FORK_BLOCK); + + sonic = BridgeTestLib.createConfigSonic(vm, forkSonic, TEST_DELEGATOR); + avalanche = BridgeTestLib.createConfigAvalanche(vm, forkAvalanche, TEST_DELEGATOR); + plasma = BridgeTestLib.createConfigPlasma(vm, forkPlasma, TEST_DELEGATOR); + } + + // ------------------- Create adapter and bridged token + adapter = TokenOFTAdapter(BridgeTestLib.setupTokenOFTAdapterOnSonic(vm, sonic)); + bridgedTokenAvalanche = BridgedToken(BridgeTestLib.setupBridgedMainToken(vm, avalanche)); + bridgedTokenPlasma = BridgedToken(BridgeTestLib.setupBridgedMainToken(vm, plasma)); + + vm.selectFork(avalanche.fork); + assertEq(bridgedTokenAvalanche.owner(), avalanche.multisig, "multisig is owner"); + vm.selectFork(plasma.fork); + assertEq(bridgedTokenPlasma.owner(), plasma.multisig, "multisig is owner"); + vm.selectFork(sonic.fork); + assertEq(adapter.owner(), sonic.multisig, "sonic.multisig is owner"); + + sonic.oapp = address(adapter); + avalanche.oapp = address(bridgedTokenAvalanche); + plasma.oapp = address(bridgedTokenPlasma); + + // ------------------- Set up Sonic:Avalanche + BridgeTestLib.setUpSonicAvalanche(vm, sonic, avalanche); + + // ------------------- Set up Sonic:Plasma + BridgeTestLib.setUpSonicPlasma(vm, sonic, plasma); + + // ------------------- Set up Avalanche:Plasma + BridgeTestLib.setUpAvalanchePlasma(vm, avalanche, plasma); + } + + //region ------------------------------------- Unit tests for bridgedTokenAvalanche + function testConfigBridgedToken() internal { + // _getConfig( + // avalanche.fork, + // AvalancheConstantsLib.LAYER_ZERO_V2_ENDPOINT, + // address(bridgedToken), + // AvalancheConstantsLib.LAYER_ZERO_V2_RECEIVE_ULN_302, + // SonicConstantsLib.LAYER_ZERO_V2_ENDPOINT_ID, + // CONFIG_TYPE_EXECUTOR + // ); + + BridgeTestLib._getConfig( + vm, + avalanche.fork, + AvalancheConstantsLib.LAYER_ZERO_V2_ENDPOINT, + address(bridgedTokenAvalanche), + AvalancheConstantsLib.LAYER_ZERO_V2_RECEIVE_ULN_302, + SonicConstantsLib.LAYER_ZERO_V2_ENDPOINT_ID, + BridgeTestLib.CONFIG_TYPE_ULN + ); + } + + function testViewBridgedToken() public { + vm.selectFork(avalanche.fork); + + // console.log("erc7201:stability.BridgedToken"); + // console.logBytes32( + // keccak256(abi.encode(uint(keccak256("erc7201:stability.BridgedToken")) - 1)) & ~bytes32(uint(0xff)) + // ); + + assertEq(bridgedTokenAvalanche.name(), "Stability STBL"); + assertEq(bridgedTokenAvalanche.symbol(), "STBL"); + assertEq(bridgedTokenAvalanche.decimals(), 18); + + assertEq(bridgedTokenAvalanche.platform(), avalanche.platform, "BridgedToken - platform"); + assertEq(bridgedTokenAvalanche.owner(), avalanche.multisig, "BridgedToken - owner"); + assertEq(bridgedTokenAvalanche.token(), address(bridgedTokenAvalanche), "BridgedToken - token"); + assertEq(bridgedTokenAvalanche.approvalRequired(), false, "BridgedToken - approvalRequired"); + assertEq( + bridgedTokenAvalanche.sharedDecimals(), BridgeTestLib.SHARED_DECIMALS, "BridgedToken - shared decimals" + ); + } + + function testBridgedTokenPause() public { + vm.selectFork(avalanche.fork); + + assertEq(bridgedTokenAvalanche.paused(address(this)), false); + + // ------------------- Pause/unpause individual address (this) + vm.prank(avalanche.multisig); + bridgedTokenAvalanche.setPaused(address(this), true); + assertEq(bridgedTokenAvalanche.paused(address(this)), true); + + vm.prank(address(this)); + vm.expectRevert(IControllable.NotOperator.selector); + bridgedTokenAvalanche.setPaused(address(this), true); + + vm.prank(avalanche.multisig); + bridgedTokenAvalanche.setPaused(address(this), false); + assertEq(bridgedTokenAvalanche.paused(address(this)), false); + } + + function testBridgedTokenSetPeers() public { + vm.selectFork(sonic.fork); + + vm.prank(address(this)); + vm.expectRevert(); + adapter.setPeer( + AvalancheConstantsLib.LAYER_ZERO_V2_ENDPOINT_ID, bytes32(uint(uint160(address(bridgedTokenAvalanche)))) + ); + + vm.prank(sonic.multisig); + adapter.setPeer( + AvalancheConstantsLib.LAYER_ZERO_V2_ENDPOINT_ID, bytes32(uint(uint160(address(bridgedTokenAvalanche)))) + ); + } + + function testSetNameSymbol() public { + vm.selectFork(avalanche.fork); + + assertEq(bridgedTokenAvalanche.name(), "Stability STBL", "BridgedToken - default name"); + assertEq(bridgedTokenAvalanche.symbol(), "STBL", "BridgedToken - default symbol"); + + string memory newName = "Bridged Stability Token"; + string memory newSymbol = "bSTBL"; + + vm.expectRevert(IControllable.NotOperator.selector); + vm.prank(address(this)); + bridgedTokenAvalanche.setName(newName); + + vm.expectRevert(IControllable.NotOperator.selector); + vm.prank(address(this)); + bridgedTokenAvalanche.setSymbol(newName); + + vm.prank(avalanche.multisig); + bridgedTokenAvalanche.setName(newName); + + vm.prank(avalanche.multisig); + bridgedTokenAvalanche.setSymbol(newSymbol); + + assertEq(bridgedTokenAvalanche.name(), newName, "BridgedToken - changed name"); + assertEq(bridgedTokenAvalanche.symbol(), newSymbol, "BridgedToken - changed symbol"); + } + + //endregion ------------------------------------- Unit tests for bridgetSTBL + + //region ------------------------------------- Unit tests for TokenOFTAdapter + function testViewTokenOFTAdapter() public { + vm.selectFork(sonic.fork); + + // console.log("erc7201:stability.TokenOFTAdapter"); + // console.logBytes32( + // keccak256(abi.encode(uint(keccak256("erc7201:stability.TokenOFTAdapter")) - 1)) & ~bytes32(uint(0xff)) + // ); + + assertEq(adapter.platform(), SonicConstantsLib.PLATFORM, "TokenOFTAdapter - platform"); + assertEq(adapter.owner(), sonic.multisig, "TokenOFTAdapter - owner"); + assertEq(adapter.token(), SonicConstantsLib.TOKEN_STBL, "TokenOFTAdapter - token"); + assertEq(adapter.approvalRequired(), true, "TokenOFTAdapter - approvalRequired"); + assertEq(adapter.sharedDecimals(), BridgeTestLib.SHARED_DECIMALS, "TokenOFTAdapter - shared decimals"); + } + + function testConfigTokenOFTAdapter() internal { + BridgeTestLib._getConfig( + vm, + sonic.fork, + SonicConstantsLib.LAYER_ZERO_V2_ENDPOINT, + address(adapter), + SonicConstantsLib.LAYER_ZERO_V2_SEND_ULN_302, + AvalancheConstantsLib.LAYER_ZERO_V2_ENDPOINT_ID, + BridgeTestLib.CONFIG_TYPE_EXECUTOR + ); + + // _getConfig( + // sonic.fork, + // SonicConstantsLib.LAYER_ZERO_V2_ENDPOINT, + // address(adapter), + // SonicConstantsLib.LAYER_ZERO_V2_SEND_ULN_302, + // AvalancheConstantsLib.LAYER_ZERO_V2_ENDPOINT_ID, + // CONFIG_TYPE_ULN + // ); + } + + function testAdapterPause() public { + vm.selectFork(sonic.fork); + + assertEq(adapter.paused(address(this)), false); + + vm.prank(sonic.multisig); + adapter.setPaused(address(this), true); + assertEq(adapter.paused(address(this)), true); + + vm.prank(address(this)); + vm.expectRevert(IControllable.NotOperator.selector); + adapter.setPaused(address(this), true); + + vm.prank(sonic.multisig); + adapter.setPaused(address(this), false); + assertEq(adapter.paused(address(this)), false); + } + + function testTokenOFTAdapterPeers() public { + vm.selectFork(avalanche.fork); + + vm.prank(address(this)); + vm.expectRevert(); + bridgedTokenAvalanche.setPeer( + SonicConstantsLib.LAYER_ZERO_V2_ENDPOINT_ID, bytes32(uint(uint160(address(adapter)))) + ); + + vm.prank(avalanche.multisig); + bridgedTokenAvalanche.setPeer( + SonicConstantsLib.LAYER_ZERO_V2_ENDPOINT_ID, bytes32(uint(uint160(address(adapter)))) + ); + } + + //endregion ------------------------------------- Unit tests for TokenOFTAdapter + + //region ------------------------------------- Test: Send from Sonic to Avalanche + function fixtureDataSA() public returns (TestCaseSendToTarget[] memory) { + TestCaseSendToTarget[] memory tests = new TestCaseSendToTarget[](3); + + tests[0] = TestCaseSendToTarget({ + sender: address(this), sendAmount: 1e18, initialBalance: 800e18, receiver: address(this) + }); + + tests[1] = TestCaseSendToTarget({ + sender: address(this), sendAmount: 799_000e18, initialBalance: 800_000e18, receiver: address(this) + }); + + tests[2] = TestCaseSendToTarget({ + sender: address(this), sendAmount: 799_000e18, initialBalance: 800_000e18, receiver: makeAddr("111") + }); + + return tests; + } + + function tableDataSATest(TestCaseSendToTarget memory dataSA) public { + _testSendToAvalancheAndCheck(dataSA.sender, dataSA.sendAmount, dataSA.initialBalance, dataSA.receiver); + } + //endregion ------------------------------------- Test: Send from Sonic to Avalanche + + //region ------------------------------------- Test: Send from Sonic to target and back + + function testSendFromSonicToAvalancheAndBack() public { + // ------------- There are 4 users: A, B, C, D + address userA = makeAddr("A"); + address userB = makeAddr("B"); + address userC = makeAddr("C"); + address userD = makeAddr("D"); + + // ------------- Sonic.A => Avalanche.B + Results memory r1 = _testSendFromSonicToBridged(userA, 157e18, 357e18, userB, avalanche); + + assertEq(r1.srcAfter.balanceSenderMainToken, 357e18 - 157e18, "A balance 1"); + assertEq(r1.targetAfter.balanceReceiverMainToken, 157e18, "B balance 1"); + + // ------------- Avalanche.B => Avalanche.C + vm.selectFork(avalanche.fork); + vm.prank(userB); + IERC20(bridgedTokenAvalanche).safeTransfer(userC, 100e18); + + assertEq(bridgedTokenAvalanche.balanceOf(userB), 57e18, "B balance 2"); + assertEq(bridgedTokenAvalanche.balanceOf(userC), 100e18, "C balance 2"); + + // ------------- Avalanche.C => Sonic.D + Results memory r2 = _testSendFromBridgedToSonic(userC, 80e18, userD, avalanche); + + assertEq(r2.srcAfter.balanceSenderMainToken, 20e18, "C balance 3"); + assertEq(r2.targetAfter.balanceReceiverMainToken, 80e18, "D balance 3"); + + assertEq(r2.srcAfter.totalSupplyMainToken, 57e18 + 20e18, "total supply after all transfers: b + c"); + assertEq( + r2.targetAfter.totalSupplyMainToken, + r1.srcBefore.totalSupplyMainToken, + "total supply of STBL wasn't changed" + ); + } + + function testSendFromSonicToPlasmaAndBack() public { + // ------------- There are 4 users: A, B, C, D + address userA = makeAddr("A"); + address userB = makeAddr("B"); + address userC = makeAddr("C"); + address userD = makeAddr("D"); + + // ------------- Sonic.A => Plasma.B + Results memory r1 = _testSendFromSonicToBridged(userA, 157e18, 357e18, userB, plasma); + + assertEq(r1.srcAfter.balanceSenderMainToken, 357e18 - 157e18, "A balance 1"); + assertEq(r1.targetAfter.balanceReceiverMainToken, 157e18, "B balance 1"); + + // ------------- Plasma.B => Plasma.C + vm.selectFork(plasma.fork); + vm.prank(userB); + IERC20(plasma.oapp).safeTransfer(userC, 100e18); + + assertEq(IERC20(plasma.oapp).balanceOf(userB), 57e18, "B balance 2"); + assertEq(IERC20(plasma.oapp).balanceOf(userC), 100e18, "C balance 2"); + + // ------------- Plasma.C => Sonic.D + Results memory r2 = _testSendFromBridgedToSonic(userC, 80e18, userD, plasma); + + assertEq(r2.srcAfter.balanceSenderMainToken, 20e18, "C balance 3"); + assertEq(r2.targetAfter.balanceReceiverMainToken, 80e18, "D balance 3"); + + assertEq(r2.srcAfter.totalSupplyMainToken, 57e18 + 20e18, "total supply after all transfers: b + c"); + assertEq( + r2.targetAfter.totalSupplyMainToken, + r1.srcBefore.totalSupplyMainToken, + "total supply of STBL wasn't changed" + ); + } + + function testSendFromAvalancheToPlasmaAndBack() public { + // ------------- There are 4 users: A, B, C, D + address userA = makeAddr("A"); + address userB = makeAddr("B"); + address userC = makeAddr("C"); + + // ------------- Sonic.A => Plasma.B + Results memory r1 = _testSendFromSonicToBridged(userA, 157e18, 357e18, userB, plasma); + + assertEq(r1.srcAfter.balanceSenderMainToken, 357e18 - 157e18, "A balance 1"); + assertEq(r1.targetAfter.balanceReceiverMainToken, 157e18, "B balance 1"); + + // ------------- Plasma.B => Avalanche.C + Results memory r2 = _testSendFromBridgedToBridged(userB, 57e18, userC, plasma, avalanche); + + assertEq(r2.srcAfter.balanceSenderMainToken, 100e18, "B balance on plasma 2"); + assertEq(r2.targetAfter.balanceReceiverMainToken, 57e18, "C balance on avalanche 2"); + + // ------------- Avalanche.C => Plasma.C + Results memory r3 = _testSendFromBridgedToBridged(userC, 27e18, userC, avalanche, plasma); + // _showResults(r3.srcBefore); + // _showResults(r3.srcAfter); + // _showResults(r3.targetBefore); + // _showResults(r3.targetAfter); + + assertEq(r3.srcAfter.balanceReceiverMainToken, 30e18, "C balance on avalanche 3"); + assertEq(r3.targetAfter.balanceSenderMainToken, 27e18, "C balance on plasma 3"); + + // ------------- Avalanche.C => Sonic.A + Results memory r4 = _testSendFromBridgedToSonic(userC, 20e18, userC, avalanche); + + assertEq(r4.srcAfter.balanceReceiverMainToken, 10e18, "C balance on Avalanche 4"); + assertEq(r4.targetAfter.balanceSenderMainToken, 20e18, "C balance on Sonic 4"); + } + + function testUserPausedOnSonic() public { + address userF = makeAddr("A"); + address userA = makeAddr("D"); + + // ------------- Prepare balances and pause the user on Sonic + _testSendFromSonicToBridged(userF, 100e18, 500e18, userF, avalanche); + + vm.selectFork(sonic.fork); + deal(SonicConstantsLib.TOKEN_STBL, userA, 300e18); + + assertEq(IERC20(SonicConstantsLib.TOKEN_STBL).balanceOf(userF), 400e18, "Sonic.F: initial balance"); + assertEq(IERC20(SonicConstantsLib.TOKEN_STBL).balanceOf(userA), 300e18, "Sonic.A: initial balance"); + + vm.prank(sonic.multisig); + adapter.setPaused(userF, true); + + vm.selectFork(avalanche.fork); + vm.prank(userF); + IERC20(bridgedTokenAvalanche).safeTransfer(userA, 70e18); + + assertEq(bridgedTokenAvalanche.balanceOf(userF), 30e18, "Avalanche.F: initial balance"); + assertEq(bridgedTokenAvalanche.balanceOf(userA), 70e18, "Avalanche.A: initial balance"); + + // ----------- Tests + _testSendToAvalancheOnPause(userF, 1e18, userA, false); // forbidden + _testSendToAvalancheOnPause(userA, 1e18, userF, true); // allowed + _testSendToSonicOnPause(userF, 1e18, userA, true); // allowed + _testSendToSonicOnPause(userA, 1e18, userF, true); // allowed + } + + function testWholeBridgePausedOnSonic() public { + address userF = makeAddr("A"); + address userA = makeAddr("D"); + + // ------------- Prepare balances and pause the user on Sonic + _testSendFromSonicToBridged(userF, 100e18, 500e18, userF, avalanche); + + vm.selectFork(sonic.fork); + deal(SonicConstantsLib.TOKEN_STBL, userA, 300e18); + + assertEq(IERC20(SonicConstantsLib.TOKEN_STBL).balanceOf(userF), 400e18, "Sonic.F: initial balance"); + assertEq(IERC20(SonicConstantsLib.TOKEN_STBL).balanceOf(userA), 300e18, "Sonic.A: initial balance"); + + // ------------ Pause whole bridge on Sonic + vm.prank(sonic.multisig); + adapter.setPaused(address(adapter), true); + + vm.selectFork(avalanche.fork); + vm.prank(userF); + IERC20(bridgedTokenAvalanche).safeTransfer(userA, 70e18); + + assertEq(bridgedTokenAvalanche.balanceOf(userF), 30e18, "Avalanche.F: initial balance"); + assertEq(bridgedTokenAvalanche.balanceOf(userA), 70e18, "Avalanche.A: initial balance"); + + // ----------- Tests + _testSendToAvalancheOnPause(userF, 1e18, userA, false); // forbidden + _testSendToAvalancheOnPause(userA, 1e18, userF, false); // forbidden + _testSendToSonicOnPause(userF, 1e18, userA, true); // allowed + _testSendToSonicOnPause(userA, 1e18, userF, true); // allowed + } + + function testUserPausedOnAvalanche() public { + address userF = makeAddr("A"); + address userA = makeAddr("D"); + + // ------------- Prepare balances and pause the user on Avalanche + _testSendFromSonicToBridged(userF, 100e18, 500e18, userF, avalanche); + + vm.selectFork(sonic.fork); + deal(SonicConstantsLib.TOKEN_STBL, userA, 300e18); + + assertEq(IERC20(SonicConstantsLib.TOKEN_STBL).balanceOf(userF), 400e18, "Sonic.F: initial balance"); + assertEq(IERC20(SonicConstantsLib.TOKEN_STBL).balanceOf(userA), 300e18, "Sonic.A: initial balance"); + + vm.selectFork(avalanche.fork); + vm.prank(userF); + IERC20(bridgedTokenAvalanche).safeTransfer(userA, 70e18); + + assertEq(bridgedTokenAvalanche.balanceOf(userF), 30e18, "Avalanche.F: initial balance"); + assertEq(bridgedTokenAvalanche.balanceOf(userA), 70e18, "Avalanche.A: initial balance"); + + vm.prank(avalanche.multisig); + bridgedTokenAvalanche.setPaused(userF, true); + + // ----------- Tests + _testSendToAvalancheOnPause(userF, 1e18, userA, true); // allowed + _testSendToAvalancheOnPause(userA, 1e18, userF, true); // allowed + _testSendToSonicOnPause(userF, 1e18, userA, false); // forbidden + _testSendToSonicOnPause(userA, 1e18, userF, true); // allowed + } + + function testWholeBridgePausedOnAvalanche() public { + address userF = makeAddr("A"); + address userA = makeAddr("D"); + + // ------------- Prepare balances and pause the user on Avalanche + _testSendFromSonicToBridged(userF, 100e18, 500e18, userF, avalanche); + + vm.selectFork(sonic.fork); + deal(SonicConstantsLib.TOKEN_STBL, userA, 300e18); + + assertEq(IERC20(SonicConstantsLib.TOKEN_STBL).balanceOf(userF), 400e18, "Sonic.F: initial balance"); + assertEq(IERC20(SonicConstantsLib.TOKEN_STBL).balanceOf(userA), 300e18, "Sonic.A: initial balance"); + + vm.selectFork(avalanche.fork); + vm.prank(userF); + IERC20(bridgedTokenAvalanche).safeTransfer(userA, 70e18); + + assertEq(bridgedTokenAvalanche.balanceOf(userF), 30e18, "Avalanche.F: initial balance"); + assertEq(bridgedTokenAvalanche.balanceOf(userA), 70e18, "Avalanche.A: initial balance"); + + // ------------ Pause whole bridge on Avalanche + vm.prank(avalanche.multisig); + bridgedTokenAvalanche.setPaused(address(bridgedTokenAvalanche), true); + + // ----------- Tests + _testSendToAvalancheOnPause(userF, 1e18, userA, true); // allowed + _testSendToAvalancheOnPause(userA, 1e18, userF, true); // allowed + _testSendToSonicOnPause(userF, 1e18, userA, false); // forbidden + _testSendToSonicOnPause(userA, 1e18, userF, false); // forbidden + } + + function testUserPausedOnBothChains() public { + address userF = makeAddr("A"); + address userA = makeAddr("D"); + + // ------------- Prepare balance and pause the user on both chains + _testSendFromSonicToBridged(userF, 100e18, 500e18, userF, avalanche); + + vm.selectFork(sonic.fork); + deal(SonicConstantsLib.TOKEN_STBL, userA, 300e18); + + assertEq(IERC20(SonicConstantsLib.TOKEN_STBL).balanceOf(userF), 400e18, "Sonic.F: initial balance"); + assertEq(IERC20(SonicConstantsLib.TOKEN_STBL).balanceOf(userA), 300e18, "Sonic.A: initial balance"); + + vm.prank(sonic.multisig); + adapter.setPaused(userF, true); + + vm.selectFork(avalanche.fork); + vm.prank(userF); + IERC20(bridgedTokenAvalanche).safeTransfer(userA, 70e18); + + assertEq(bridgedTokenAvalanche.balanceOf(userF), 30e18, "Avalanche.F: initial balance"); + assertEq(bridgedTokenAvalanche.balanceOf(userA), 70e18, "Avalanche.A: initial balance"); + + vm.prank(avalanche.multisig); + bridgedTokenAvalanche.setPaused(userF, true); + + // ----------- Tests + _testSendToAvalancheOnPause(userF, 1e18, userA, false); // forbidden + _testSendToAvalancheOnPause(userA, 1e18, userF, true); // allowed + _testSendToSonicOnPause(userF, 1e18, userA, false); // forbidden + _testSendToSonicOnPause(userA, 1e18, userF, true); // allowed + } + + function testContractsPausedOnBothChains() public { + address userF = makeAddr("A"); + address userA = makeAddr("D"); + + // ------------- Prepare balance and pause the user on both chains + _testSendFromSonicToBridged(userF, 100e18, 500e18, userF, avalanche); + + vm.selectFork(sonic.fork); + deal(SonicConstantsLib.TOKEN_STBL, userA, 300e18); + + assertEq(IERC20(SonicConstantsLib.TOKEN_STBL).balanceOf(userF), 400e18, "Sonic.F: initial balance"); + assertEq(IERC20(SonicConstantsLib.TOKEN_STBL).balanceOf(userA), 300e18, "Sonic.A: initial balance"); + + vm.prank(sonic.multisig); + adapter.setPaused(address(adapter), true); + + vm.selectFork(avalanche.fork); + vm.prank(userF); + IERC20(bridgedTokenAvalanche).safeTransfer(userA, 70e18); + + assertEq(bridgedTokenAvalanche.balanceOf(userF), 30e18, "Avalanche.F: initial balance"); + assertEq(bridgedTokenAvalanche.balanceOf(userA), 70e18, "Avalanche.A: initial balance"); + + vm.prank(avalanche.multisig); + bridgedTokenAvalanche.setPaused(address(bridgedTokenAvalanche), true); + + // ----------- Tests + _testSendToAvalancheOnPause(userF, 1e18, userA, false); // forbidden + _testSendToAvalancheOnPause(userA, 1e18, userF, false); // forbidden + _testSendToSonicOnPause(userF, 1e18, userA, false); // forbidden + _testSendToSonicOnPause(userA, 1e18, userF, false); // forbidden + + // ------------ Unpause + vm.selectFork(sonic.fork); + vm.prank(sonic.multisig); + adapter.setPaused(address(adapter), false); + + vm.selectFork(avalanche.fork); + vm.prank(avalanche.multisig); + bridgedTokenAvalanche.setPaused(address(bridgedTokenAvalanche), false); + + // ----------- Tests + _testSendToAvalancheOnPause(userF, 1e18, userA, true); // allowed + _testSendToAvalancheOnPause(userA, 1e18, userF, true); // allowed + _testSendToSonicOnPause(userF, 1e18, userA, true); // allowed + _testSendToSonicOnPause(userA, 1e18, userF, true); // allowed + } + + //endregion ------------------------------------- Test: Send from Sonic to Avalanche and back + + //region ------------------------------------- Test implementation + function _testSendToAvalancheAndCheck(address sender, uint sendAmount, uint balance0, address receiver) internal { + uint shapshot = vm.snapshotState(); + + Results memory r = _testSendFromSonicToBridged(sender, sendAmount, balance0, receiver, avalanche); + + assertEq(r.srcBefore.balanceSenderMainToken, balance0, "sender's initial STBL balance"); + assertEq(r.srcBefore.balanceContractMainToken, 0, "no tokens in adapter initially"); + assertEq(r.srcAfter.balanceSenderMainToken, balance0 - sendAmount, "sender's final STBL balance"); + assertEq(r.srcAfter.balanceContractMainToken, sendAmount, "all tokens are in adapter"); + + assertEq(r.targetBefore.balanceReceiverMainToken, 0, "receiver has no tokens on avalanche initially"); + assertEq(r.targetAfter.balanceReceiverMainToken, sendAmount, "receiver has received expected amount"); + + assertEq(r.srcBefore.balanceSenderEther, r.srcAfter.balanceSenderEther + r.nativeFee, "expected fee"); + vm.revertToState(shapshot); + } + + /// @notice Sends tokens from Sonic to Target chain + function _testSendFromSonicToBridged( + address sender, + uint sendAmount, + uint balance0, + address receiver, + BridgeTestLib.ChainConfig memory target + ) internal returns (Results memory dest) { + vm.selectFork(sonic.fork); + + // ------------------- Prepare user tokens + deal(sender, 1 ether); // to pay fees + deal(SonicConstantsLib.TOKEN_STBL, sender, balance0); + + vm.prank(sender); + IERC20(SonicConstantsLib.TOKEN_STBL).approve(address(adapter), sendAmount); + + // ------------------- Prepare send options + bytes memory options = adapter.buildOptions(GAS_LIMIT, 0); + // bytes memory options = OptionsBuilder.newOptions().addExecutorLzReceiveOption(GAS_LIMIT, 0); + + SendParam memory sendParam = SendParam({ + dstEid: target.endpointId, + to: bytes32(uint(uint160(receiver))), + amountLD: sendAmount, + minAmountLD: sendAmount, + extraOptions: options, + composeMsg: "", + oftCmd: "" + }); + MessagingFee memory msgFee = adapter.quoteSend(sendParam, false); + // console.log("Quoted native fee:", msgFee.nativeFee); + + dest.srcBefore = _getBalancesSonic(sender, receiver); + + // ------------------- Send + vm.recordLogs(); + + vm.prank(sender); + adapter.send{value: msgFee.nativeFee}(sendParam, msgFee, sender); + (bytes memory message,) = BridgeTestLib._extractSendMessage(vm.getRecordedLogs()); + + // ------------------ Target: simulate message reception + vm.selectFork(target.fork); + dest.targetBefore = _getBalancesBridged(sender, receiver, target); + + Origin memory origin = Origin({ + srcEid: SonicConstantsLib.LAYER_ZERO_V2_ENDPOINT_ID, + sender: bytes32(uint(uint160(address(adapter)))), + nonce: 1 + }); + + { + uint gasBefore = gasleft(); + vm.prank(target.endpoint); + IOAppReceiver(target.oapp) + .lzReceive( + origin, + bytes32(0), // guid: actual value doesn't matter + message, + address(0), // executor + "" // extraData + ); + assertLt(gasBefore - gasleft(), GAS_LIMIT, "gas limit exceeded"); // ~60 ths + // console.log("gasBefore - gasleft()", gasBefore - gasleft()); + } + + dest.targetAfter = _getBalancesBridged(sender, receiver, target); + vm.selectFork(sonic.fork); + dest.srcAfter = _getBalancesSonic(sender, receiver); + + dest.nativeFee = msgFee.nativeFee; + + return dest; + } + + /// @notice Sends tokens from a target chain to Sonic + function _testSendFromBridgedToSonic( + address sender, + uint sendAmount, + address receiver, + BridgeTestLib.ChainConfig memory target + ) internal returns (Results memory dest) { + vm.selectFork(target.fork); + + // ------------------- Prepare user tokens + deal(sender, 1 ether); // to pay fees + + vm.prank(sender); + IERC20(target.oapp).approve(target.oapp, sendAmount); + + // ------------------- Prepare send options + bytes memory options = IOFTPausable(target.oapp).buildOptions(2_000_000, 0); + // bytes memory options = OptionsBuilder.newOptions().addExecutorLzReceiveOption(2_000_000, 0); + + SendParam memory sendParam = SendParam({ + dstEid: SonicConstantsLib.LAYER_ZERO_V2_ENDPOINT_ID, + to: bytes32(uint(uint160(receiver))), + amountLD: sendAmount, + minAmountLD: sendAmount, + extraOptions: options, + composeMsg: "", + oftCmd: "" + }); + MessagingFee memory msgFee = IOFT(target.oapp).quoteSend(sendParam, false); + + dest.srcBefore = _getBalancesBridged(sender, receiver, target); + + // ------------------- Send + vm.recordLogs(); + + vm.prank(sender); + IOFT(target.oapp).send{value: msgFee.nativeFee}(sendParam, msgFee, sender); + (bytes memory message,) = BridgeTestLib._extractSendMessage(vm.getRecordedLogs()); + + // ------------------ Sonic: simulate message reception + vm.selectFork(sonic.fork); + dest.targetBefore = _getBalancesSonic(sender, receiver); + + Origin memory origin = + Origin({srcEid: target.endpointId, sender: bytes32(uint(uint160(target.oapp))), nonce: 1}); + + vm.prank(SonicConstantsLib.LAYER_ZERO_V2_ENDPOINT); + adapter.lzReceive( + origin, + bytes32(0), // guid: actual value doesn't matter + message, + address(0), // executor + "" // extraData + ); + + dest.targetAfter = _getBalancesSonic(sender, receiver); + vm.selectFork(target.fork); + dest.srcAfter = _getBalancesBridged(sender, receiver, target); + + dest.nativeFee = msgFee.nativeFee; + + return dest; + } + + /// @notice Sends tokens from src to target chain + function _testSendFromBridgedToBridged( + address sender, + uint sendAmount, + address receiver, + BridgeTestLib.ChainConfig memory src, + BridgeTestLib.ChainConfig memory target + ) internal returns (Results memory dest) { + vm.selectFork(src.fork); + + // ------------------- Prepare user tokens + deal(sender, 1 ether); // to pay fees + // assume that the sender has enough balance + + vm.prank(sender); + IERC20(src.oapp).approve(address(adapter), sendAmount); + + // ------------------- Prepare send options + bytes memory options = IOFTPausable(src.oapp).buildOptions(GAS_LIMIT, 0); + // bytes memory options = OptionsBuilder.newOptions().addExecutorLzReceiveOption(GAS_LIMIT, 0); + + SendParam memory sendParam = SendParam({ + dstEid: target.endpointId, + to: bytes32(uint(uint160(receiver))), + amountLD: sendAmount, + minAmountLD: sendAmount, + extraOptions: options, + composeMsg: "", + oftCmd: "" + }); + MessagingFee memory msgFee = IOFT(src.oapp).quoteSend(sendParam, false); + // console.log("Quoted native fee:", msgFee.nativeFee); + + dest.srcBefore = _getBalancesBridged(sender, receiver, src); + + // ------------------- Send + vm.recordLogs(); + + vm.prank(sender); + IOFT(src.oapp).send{value: msgFee.nativeFee}(sendParam, msgFee, sender); + (bytes memory message,) = BridgeTestLib._extractSendMessage(vm.getRecordedLogs()); + + // ------------------ Target: simulate message reception + vm.selectFork(target.fork); + dest.targetBefore = _getBalancesBridged(sender, receiver, target); + + Origin memory origin = Origin({srcEid: src.endpointId, sender: bytes32(uint(uint160(src.oapp))), nonce: 1}); + + { + uint gasBefore = gasleft(); + vm.prank(target.endpoint); + IOAppReceiver(target.oapp) + .lzReceive( + origin, + bytes32(0), // guid: actual value doesn't matter + message, + address(0), // executor + "" // extraData + ); + assertLt(gasBefore - gasleft(), GAS_LIMIT, "gas limit exceeded"); + // console.log("gasBefore - gasleft()", gasBefore - gasleft()); + } + + dest.targetAfter = _getBalancesBridged(sender, receiver, target); + vm.selectFork(src.fork); + dest.srcAfter = _getBalancesBridged(sender, receiver, src); + + dest.nativeFee = msgFee.nativeFee; + + return dest; + } + + function _testSendToAvalancheOnPause( + address sender, + uint sendAmount, + address receiver, + bool expectSuccess + ) internal { + vm.selectFork(sonic.fork); + uint snapshot = vm.snapshotState(); + + deal(sender, 1 ether); // to pay fees + + vm.prank(sender); + IERC20(SonicConstantsLib.TOKEN_STBL).approve(address(adapter), sendAmount); + + SendParam memory sendParam = SendParam({ + dstEid: AvalancheConstantsLib.LAYER_ZERO_V2_ENDPOINT_ID, + to: bytes32(uint(uint160(receiver))), + amountLD: sendAmount, + minAmountLD: sendAmount, + extraOptions: adapter.buildOptions(2_000_000, 0), + composeMsg: "", + oftCmd: "" + }); + MessagingFee memory msgFee = adapter.quoteSend(sendParam, false); + + // ------------------- Send + vm.prank(sender); + if (!expectSuccess) { + vm.expectRevert(IOFTPausable.Paused.selector); + } + adapter.send{value: msgFee.nativeFee}(sendParam, msgFee, sender); + + vm.revertToState(snapshot); + } + + function _testSendToSonicOnPause(address sender, uint sendAmount, address receiver, bool expectSuccess) internal { + vm.selectFork(avalanche.fork); + uint snapshot = vm.snapshotState(); + + deal(sender, 1 ether); // to pay fees + + vm.prank(sender); + bridgedTokenAvalanche.approve(address(bridgedTokenAvalanche), sendAmount); + + SendParam memory sendParam = SendParam({ + dstEid: SonicConstantsLib.LAYER_ZERO_V2_ENDPOINT_ID, + to: bytes32(uint(uint160(receiver))), + amountLD: sendAmount, + minAmountLD: sendAmount, + extraOptions: OptionsBuilder.newOptions().addExecutorLzReceiveOption(2_000_000, 0), + composeMsg: "", + oftCmd: "" + }); + MessagingFee memory msgFee = bridgedTokenAvalanche.quoteSend(sendParam, false); + + vm.prank(sender); + if (!expectSuccess) { + vm.expectRevert(IOFTPausable.Paused.selector); + } + bridgedTokenAvalanche.send{value: msgFee.nativeFee}(sendParam, msgFee, sender); + + vm.revertToState(snapshot); + } + + //endregion ------------------------------------- Test implementation + + //region ------------------------------------- Internal logic + function _getBalancesSonic(address sender, address receiver) internal view returns (ChainResults memory res) { + res.balanceSenderMainToken = IERC20(SonicConstantsLib.TOKEN_STBL).balanceOf(sender); + res.balanceContractMainToken = IERC20(SonicConstantsLib.TOKEN_STBL).balanceOf(address(adapter)); + res.balanceReceiverMainToken = IERC20(SonicConstantsLib.TOKEN_STBL).balanceOf(receiver); + res.totalSupplyMainToken = IERC20(SonicConstantsLib.TOKEN_STBL).totalSupply(); + res.balanceSenderEther = sender.balance; + // console.log("Sonic.balanceSenderSTBL", res.balanceSenderSTBL); + // console.log("Sonic.balanceContractSTBL", res.balanceContractSTBL); + // console.log("Sonic.balanceReceiverSTBL", res.balanceReceiverSTBL); + // console.log("Sonic.totalSupplySTBL", res.totalSupplySTBL); + + return res; + } + + function _getBalancesBridged( + address sender, + address receiver, + BridgeTestLib.ChainConfig memory target + ) internal view returns (ChainResults memory res) { + res.balanceSenderMainToken = IERC20(target.oapp).balanceOf(sender); + res.balanceContractMainToken = IERC20(target.oapp).balanceOf(address(target.oapp)); + res.balanceReceiverMainToken = IERC20(target.oapp).balanceOf(receiver); + res.totalSupplyMainToken = IERC20(target.oapp).totalSupply(); + res.balanceSenderEther = sender.balance; + // console.log("Avalanche.balanceSenderSTBL", res.balanceSenderSTBL); + // console.log("Avalanche.balanceContractSTBL", res.balanceContractSTBL); + // console.log("Avalanche.balanceReceiverSTBL", res.balanceReceiverSTBL); + // console.log("Avalanche.totalSupplySTBL", res.totalSupplySTBL); + + return res; + } + + function _showResults(ChainResults memory res) internal pure { + console.log("balanceSenderSTBL:", res.balanceSenderMainToken); + console.log("balanceContractSTBL:", res.balanceContractMainToken); + console.log("balanceReceiverSTBL:", res.balanceReceiverMainToken); + console.log("totalSupplySTBL:", res.totalSupplyMainToken); + } + + //endregion ------------------------------------- Internal logic +} diff --git a/test/tokenomics/DAO.t.sol b/test/tokenomics/DAO.t.sol new file mode 100644 index 00000000..04c50a8a --- /dev/null +++ b/test/tokenomics/DAO.t.sol @@ -0,0 +1,741 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import {console} from "forge-std/console.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import {IPlatform} from "../../src/interfaces/IPlatform.sol"; +import {IControllable} from "../../src/interfaces/IControllable.sol"; +import {IDAO} from "../../src/interfaces/IDAO.sol"; +import {Proxy} from "../../src/core/proxy/Proxy.sol"; +import {SonicConstantsLib} from "../../chains/sonic/SonicConstantsLib.sol"; +import {Test} from "forge-std/Test.sol"; +import {DAO} from "../../src/tokenomics/DAO.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {Platform} from "../../src/core/Platform.sol"; + +contract DAOSonicTest is Test { + using SafeERC20 for IERC20; + + uint public constant FORK_BLOCK = 47854805; // Sep-23-2025 04:02:39 AM +UTC + address internal multisig; + + struct Powers { + uint localPower; + uint otherPower; + uint delegatedLocalPower; + uint delegatedOtherPower; + } + + constructor() { + vm.selectFork(vm.createFork(vm.envString("SONIC_RPC_URL"), FORK_BLOCK)); + multisig = IPlatform(SonicConstantsLib.PLATFORM).multisig(); + _upgradePlatform(); + + // console.logBytes32( + // keccak256(abi.encode(uint(keccak256("erc7201:stability.StabilityDAO")) - 1)) & ~bytes32(uint(0xff)) + // ); + } + + //region --------------------------------- Unit tests + + function testInitializeAndView() public { + IDAO.DaoParams memory p = IDAO.DaoParams({ + minimalPower: 4000e18, + exitPenalty: 50_00, + quorum: 20_000, + proposalThreshold: 10_000, + powerAllocationDelay: 86400 + }); + + Proxy proxy = new Proxy(); + proxy.initProxy(address(new DAO())); + + IDAO token = IDAO(address(proxy)); + token.initialize(SonicConstantsLib.PLATFORM, address(1), address(2), p, "Stability DAO", "STBL_DAO"); + + assertEq(token.xToken(), address(1)); + assertEq(token.xStaking(), address(2)); + assertEq(token.name(), "Stability DAO"); + assertEq(token.symbol(), "STBL_DAO"); + assertEq(token.decimals(), 18); + + assertEq(token.minimalPower(), p.minimalPower); + assertEq(token.exitPenalty(), p.exitPenalty); + assertEq(token.proposalThreshold(), p.proposalThreshold); + assertEq(token.powerAllocationDelay(), p.powerAllocationDelay); + assertEq(token.quorum(), p.quorum); + } + + function testMintBurn() public { + address governance = IPlatform(SonicConstantsLib.PLATFORM).governance(); + IDAO token = _createDAOInstance(); + + vm.prank(address(0x123)); + vm.expectRevert(IControllable.IncorrectMsgSender.selector); + token.mint(address(0x123), 1e18); + + vm.prank(address(0x123)); + vm.expectRevert(IControllable.IncorrectMsgSender.selector); + token.burn(address(0x123), 1e18); + + vm.prank(multisig); + vm.expectRevert(IControllable.IncorrectMsgSender.selector); + token.mint(address(0x123), 1e18); + + vm.prank(multisig); + vm.expectRevert(IControllable.IncorrectMsgSender.selector); + token.burn(address(0x123), 1e18); + + vm.prank(governance); + vm.expectRevert(IControllable.IncorrectMsgSender.selector); + token.mint(address(0x123), 1e18); + + vm.prank(governance); + vm.expectRevert(IControllable.IncorrectMsgSender.selector); + token.burn(address(0x123), 1e18); + + vm.prank(token.xStaking()); + token.mint(address(0x123), 1e18); + assertEq(token.balanceOf(address(0x123)), 1e18); + + vm.prank(token.xStaking()); + token.burn(address(0x123), 0.5e18); + assertEq(token.balanceOf(address(0x123)), 0.5e18); + + vm.prank(token.xStaking()); + token.burn(address(0x123), 0.5e18); + assertEq(token.balanceOf(address(0x123)), 0); + } + + function testUpdateConfig() public { + IDAO.DaoParams memory p1 = IDAO.DaoParams({ + minimalPower: 4000e18, + exitPenalty: 50_00, // 50% + quorum: 20_000, // 20% + proposalThreshold: 10_000, // 10% + powerAllocationDelay: 86400 + }); + + IDAO.DaoParams memory p2 = IDAO.DaoParams({ + minimalPower: 5000e18, + exitPenalty: 80_00, // 80% + quorum: 35_000, // 35% + proposalThreshold: 20_000, // 20% + powerAllocationDelay: 172800 + }); + + IDAO token = _createDAOInstance(p1); + + vm.prank(multisig); + IPlatform(SonicConstantsLib.PLATFORM).setupStabilityDAO(address(token)); + + IDAO.DaoParams memory config = token.config(); + assertEq(config.minimalPower, p1.minimalPower, "minimalPower"); + assertEq(config.exitPenalty, p1.exitPenalty, "exitPenalty"); + assertEq(config.proposalThreshold, p1.proposalThreshold, "proposalThreshold"); + assertEq(config.powerAllocationDelay, p1.powerAllocationDelay, "powerAllocationDelay"); + assertEq(config.quorum, p1.quorum, "quorum"); + + vm.prank(address(0x123)); + vm.expectRevert(IControllable.NotGovernanceAndNotMultisig.selector); + token.updateConfig(p2); + + vm.prank(token.xStaking()); + vm.expectRevert(IControllable.NotGovernanceAndNotMultisig.selector); + token.updateConfig(p2); + + config = _updateConfig(token, multisig, p2); + + assertEq(config.minimalPower, p2.minimalPower, "minimalPower2"); + assertEq(config.exitPenalty, p2.exitPenalty, "exitPenalty2"); + assertEq(config.proposalThreshold, p2.proposalThreshold, "proposalThreshold2"); + assertEq(config.powerAllocationDelay, p2.powerAllocationDelay, "powerAllocationDelay2"); + assertEq(config.quorum, p2.quorum, "quorum2"); + + config = _updateConfig(token, IPlatform(SonicConstantsLib.PLATFORM).governance(), p2); + + assertEq(config.minimalPower, p2.minimalPower, "minimalPower3"); + assertEq(config.exitPenalty, p2.exitPenalty, "exitPenalty3"); + assertEq(config.proposalThreshold, p2.proposalThreshold, "proposalThreshold3"); + assertEq(config.powerAllocationDelay, p2.powerAllocationDelay, "powerAllocationDelay3"); + assertEq(config.quorum, p2.quorum, "quorum3"); + } + + function testUpdateConfigBadPaths() public { + IDAO.DaoParams memory p1 = IDAO.DaoParams({ + minimalPower: 4000e18, + exitPenalty: 50_00, // 50% + quorum: 20_000, // 20% + proposalThreshold: 10_000, // 10% + powerAllocationDelay: 86400 + }); + IDAO token = _createDAOInstance(p1); + + p1.proposalThreshold = 100_000; // 100% + + vm.prank(multisig); + vm.expectRevert(IDAO.WrongValue.selector); + token.updateConfig(p1); + + p1.proposalThreshold = 10_000; + p1.exitPenalty = 100_00; // 100% + + vm.prank(multisig); + vm.expectRevert(IDAO.WrongValue.selector); + token.updateConfig(p1); + + p1.exitPenalty = 50_00; + p1.quorum = 100_000; // 100% + + vm.prank(multisig); + vm.expectRevert(IDAO.WrongValue.selector); + token.updateConfig(p1); + } + + function testNonTransferable() public { + IDAO token = _createDAOInstance(); + + vm.prank(token.xStaking()); + token.mint(address(0x123), 1e18); + + vm.prank(address(0x123)); + vm.expectRevert(IDAO.NonTransferable.selector); + // slither-disable-next-line erc20-unchecked-transfer + // forge-lint: disable-next-line(erc20-unchecked-transfer) + token.transfer(address(0x456), 1e18); + + vm.prank(address(0x123)); + token.approve(address(0x456), 1e18); + + vm.prank(address(0x456)); + vm.expectRevert(IDAO.NonTransferable.selector); + // slither-disable-next-line erc20-unchecked-transfer + // forge-lint: disable-next-line(erc20-unchecked-transfer) + token.transferFrom(address(0x123), address(0x789), 1e18); + } + + function testSetPowerDelegation() public { + address user1 = address(1); + address user2 = address(2); + IDAO dao = _createDAOInstance(); + + // ---------------------------- Initial state + vm.prank(dao.xStaking()); + dao.mint(user1, 10_000e18); + + vm.prank(dao.xStaking()); + dao.mint(user2, 20_000e18); + + assertEq(dao.getVotes(user1), 10_000e18); + assertEq(dao.getVotes(user2), 20_000e18); + + (address delegatedTo, address[] memory delegates) = dao.delegates(user1); + assertEq(delegatedTo, address(0)); + assertEq(delegates.length, 0); + + // ---------------------------- User 1 delegates to User 2 + vm.prank(user1); + dao.setPowerDelegation(user2); + + vm.expectRevert(IDAO.AlreadyDelegated.selector); + vm.prank(user1); + dao.setPowerDelegation(address(this)); + + assertEq(dao.getVotes(user1), 0); + assertEq(dao.getVotes(user2), 20_000e18 + 10_000e18); + + (delegatedTo, delegates) = dao.delegates(user1); + assertEq(delegatedTo, user2); + assertEq(delegates.length, 0); + + (delegatedTo, delegates) = dao.delegates(user2); + assertEq(delegatedTo, address(0)); + assertEq(delegates.length, 1); + assertEq(delegates[0], user1); + + // ---------------------------- User 2 delegates to User 1 + vm.prank(user2); + dao.setPowerDelegation(user1); + + assertEq(dao.getVotes(user1), 20_000e18); + assertEq(dao.getVotes(user2), 10_000e18); + + (delegatedTo, delegates) = dao.delegates(user1); + assertEq(delegatedTo, user2); + assertEq(delegates.length, 1); + assertEq(delegates[0], user2); + + (delegatedTo, delegates) = dao.delegates(user2); + assertEq(delegatedTo, user1); + assertEq(delegates.length, 1); + assertEq(delegates[0], user1); + + // ---------------------------- Both Users clear delegations + vm.prank(user1); + dao.setPowerDelegation(user1); + + vm.prank(user2); + dao.setPowerDelegation(address(0)); + + assertEq(dao.getVotes(user1), 10_000e18); + assertEq(dao.getVotes(user2), 20_000e18); + + (delegatedTo, delegates) = dao.delegates(user1); + assertEq(delegatedTo, address(0)); + assertEq(delegates.length, 0); + + (delegatedTo, delegates) = dao.delegates(user2); + assertEq(delegatedTo, address(0)); + assertEq(delegates.length, 0); + } + + function testDelegationForbidden() public { + IDAO token = _createDAOInstance(); + + // ---------------------- initially user delegates power to other user + assertEq(token.delegationForbidden(), false, "delegation is allowed initially"); + + address user2 = makeAddr("to"); + + token.setPowerDelegation(user2); + + { + (address delegatedTo,) = token.delegates(address(this)); + assertEq(delegatedTo, user2, "delegated to 1"); + } + + // ---------------------- Forbid delegation + vm.prank(multisig); + token.setDelegationForbidden(true); + + assertEq(token.delegationForbidden(), true, "delegation is forbidden now"); + + // ---------------------- User is not able to re-delegate power to another user + vm.expectRevert(IDAO.DelegationForbiddenOnTheChain.selector); + token.setPowerDelegation(makeAddr("to2")); + + { + (address delegatedTo,) = token.delegates(address(this)); + assertEq(delegatedTo, user2, "delegated to 2"); + } + + // ---------------------- User is able to clear exist delegation + token.setPowerDelegation(address(0)); + { + (address delegatedTo,) = token.delegates(address(this)); + assertEq(delegatedTo, address(0), "delegated to 3"); + } + } + + // solidity + function testWhitelistedForOtherChainsPowers() public { + IDAO token = _createDAOInstance(); + address user = address(0x123); + + assertEq(token.isWhitelistedForOtherChainsPowers(user), false, "initially not whitelisted"); + + vm.prank(address(0x456)); + vm.expectRevert(IControllable.NotGovernanceAndNotMultisig.selector); + token.setWhitelistedForOtherChainsPowers(user, true); + + vm.prank(multisig); + token.setWhitelistedForOtherChainsPowers(user, true); + assertEq(token.isWhitelistedForOtherChainsPowers(user), true, "whitelisted by multisig"); + + vm.prank(multisig); + token.setWhitelistedForOtherChainsPowers(user, false); + assertEq(token.isWhitelistedForOtherChainsPowers(user), false, "removed by multisig"); + } + + // solidity + function testUpdateOtherChainsPowers() public { + IDAO token = _createDAOInstance(); + + address user1 = makeAddr("user1"); + address user2 = makeAddr("user2"); + address user3 = makeAddr("user3"); + uint blockTimestamp = block.timestamp; + + // -------------------------- provide powers for user 1 and user 2 on main chain + deal(address(token), user1, 150e18); + deal(address(token), user2, 250e18); + + // -------------------------- set power on other chains for user 1 and user 2 + { + address[] memory users = new address[](2); + users[0] = user1; + users[1] = user2; + uint[] memory powers = new uint[](2); + powers[0] = 1000e18; + powers[1] = 2000e18; + + vm.prank(user1); + vm.expectRevert(IDAO.NotOtherChainsPowersWhitelisted.selector); + token.updateOtherChainsPowers(users, powers); + + vm.prank(multisig); + token.setWhitelistedForOtherChainsPowers(user1, true); + assertEq(token.isWhitelistedForOtherChainsPowers(user1), true, "user1 is whitelisted"); + + vm.prank(user1); + vm.expectRevert(IControllable.IncorrectArrayLength.selector); + token.updateOtherChainsPowers(users, new uint[](1)); + + blockTimestamp = block.timestamp; + vm.prank(user1); + token.updateOtherChainsPowers(users, powers); + + // ensure that we cannot call updateOtherChainsPowers on the same block + vm.expectRevert(IDAO.WrongValue.selector); + vm.prank(user1); + token.updateOtherChainsPowers(users, powers); + + skip(1); // in next tx we should have different timestamp because it's used as an epoch counter inside token + } + + // -------------------------- check results + { + (uint timestamp, address[] memory users, uint[] memory powers) = token.getOtherChainsPowers(); + assertEq(timestamp, blockTimestamp, "timestamp"); + assertEq(users.length, 2, "users length"); + assertEq(powers.length, 2, "powers length"); + assertEq(users[0], user1, "user1 address"); + assertEq(users[1], user2, "user2 address"); + assertEq(powers[0], 1000e18, "user1 power"); + assertEq(powers[1], 2000e18, "user2 power"); + } + + // -------------------------- set power on other chains for user 3 + { + address[] memory users = new address[](1); + users[0] = user3; + uint[] memory powers = new uint[](1); + powers[0] = 3000e18; + + blockTimestamp = block.timestamp; + vm.prank(user1); + token.updateOtherChainsPowers(users, powers); + skip(1); // in next tx we should have different timestamp because it's used as an epoch counter inside token + } + + // -------------------------- check results + { + (uint timestamp, address[] memory users, uint[] memory powers) = token.getOtherChainsPowers(); + assertEq(timestamp, blockTimestamp, "timestamp"); + assertEq(users.length, 1, "users length"); + assertEq(powers.length, 1, "powers length"); + assertEq(users[0], user3, "user3 address"); + assertEq(powers[0], 3000e18, "user3 power"); + } + } + + function testGetVotesPower() public { + IDAO token = _createDAOInstance(); + + address user1 = makeAddr("user1"); + address user2 = address(0x2); + address user3 = makeAddr("user3"); + + // -------------------------- provide powers for user 1 and user 2 on main chain + deal(address(token), user1, 150e18); + deal(address(token), user2, 250e18); + + // -------------------------- set power on other chains for user 1 and user 2 + { + address[] memory users = new address[](2); + users[0] = user1; + users[1] = user2; + uint[] memory powers = new uint[](2); + powers[0] = 1000e18; + powers[1] = 2000e18; + + vm.prank(user1); + vm.expectRevert(IDAO.NotOtherChainsPowersWhitelisted.selector); + token.updateOtherChainsPowers(users, powers); + skip(1); // in next tx we should have different timestamp because it's used as an epoch counter inside token + + vm.prank(multisig); + token.setWhitelistedForOtherChainsPowers(user1, true); + assertEq(token.isWhitelistedForOtherChainsPowers(user1), true, "user1 is whitelisted"); + + vm.prank(user1); + token.updateOtherChainsPowers(users, powers); + skip(1); // in next tx we should have different timestamp because it's used as an epoch counter inside token + } + + // -------------------------- check initial vote powers + { + assertEq(token.getVotes(user1), 150e18 + 1000e18, "1: getVotes user 1"); + assertEq(token.getVotes(user2), 250e18 + 2000e18, "1: getVotes user 2"); + + Powers memory p1 = _getPowers(token, user1); + assertEq(p1.localPower, 150e18, "1: local power of user 1"); + assertEq(p1.otherPower, 1000e18, "1: other power of user 1"); + assertEq(p1.delegatedLocalPower, 0, "1: delegated local power of user 1"); + assertEq(p1.delegatedOtherPower, 0, "1: delegated other power of user 1"); + + Powers memory p2 = _getPowers(token, user2); + assertEq(p2.localPower, 250e18, "1: local power of user 2"); + assertEq(p2.otherPower, 2000e18, "1: other power of user 2"); + assertEq(p2.delegatedLocalPower, 0, "1: delegated local power of user 2"); + assertEq(p2.delegatedOtherPower, 0, "1: delegated other power of user 2"); + } + + // -------------------------- user 1 delegates his power to user 2 + vm.prank(user1); + token.setPowerDelegation(user2); + + { + assertEq(token.getVotes(user1), 0, "2: getVotes user 1"); + assertEq(token.getVotes(user2), 250e18 + 2000e18 + 150e18 + 1000e18, "2: getVotes user 2"); + + Powers memory p1 = _getPowers(token, user1); + assertEq(p1.localPower, 150e18, "2: local power of user 1"); + assertEq(p1.otherPower, 1000e18, "2: other power of user 1 (delegated to user 2)"); + assertEq(p1.delegatedLocalPower, 0, "2: delegated local power of user 1"); + assertEq(p1.delegatedOtherPower, 0, "2: delegated other power of user 1"); + + Powers memory p2 = _getPowers(token, user2); + assertEq(p2.localPower, 250e18, "2: local power of user 2"); + assertEq(p2.otherPower, 2000e18, "2: other power of user 2"); + assertEq(p2.delegatedLocalPower, 150e18, "2: delegated local power of user 2"); + assertEq(p2.delegatedOtherPower, 1000e18, "2: delegated other power of user 2"); + } + + // -------------------------- set power on other chains for user 1 and user 3, user 3 delegates to user 2 + { + vm.prank(user3); + token.setPowerDelegation(user2); + + // assume here that user2 has lost his power on other chains + // so, user 2 is not included to updateOtherChainsPowers + + address[] memory users = new address[](2); + users[0] = user1; + users[1] = user3; + uint[] memory powers = new uint[](2); + powers[0] = 1000e18; + powers[1] = 3000e18; + + vm.prank(user1); + token.updateOtherChainsPowers(users, powers); + skip(1); // in next tx we should have different timestamp because it's used as an epoch counter inside token + } + + // -------------------------- check updated vote powers + { + assertEq(token.getVotes(user1), 0, "3: getVotes user 1"); + assertEq(token.getVotes(user2), 3000e18 + 250e18 + 150e18 + 1000e18, "3: getVotes user 2"); + assertEq(token.getVotes(user3), 0, "3: getVotes user 3"); + + Powers memory p1 = _getPowers(token, user1); + assertEq(p1.localPower, 150e18, "3: local power of user 1"); + assertEq(p1.otherPower, 1000e18, "3: other power of user 1 (delegated to user 2)"); + assertEq(p1.delegatedLocalPower, 0, "3: delegated local power of user 1"); + assertEq(p1.delegatedOtherPower, 0, "3: delegated other power of user 1"); + + Powers memory p2 = _getPowers(token, user2); + assertEq(p2.localPower, 250e18, "3: local power of user 2"); + assertEq(p2.otherPower, 0, "3: other power of user 2"); + assertEq(p2.delegatedLocalPower, 150e18, "3: delegated local power of user 2 (from user 1)"); + assertEq(p2.delegatedOtherPower, 1000e18 + 3000e18, "3: delegated other power of user 2 (from 1 and 3)"); + + Powers memory p3 = _getPowers(token, user3); + assertEq(p3.localPower, 0, "3: local power of user 3"); + assertEq(p3.otherPower, 3000e18, "3: other power of user 3 (delegated to user 2)"); + assertEq(p3.delegatedLocalPower, 0, "3: delegated local power of user 3"); + assertEq(p3.delegatedOtherPower, 0, "3: delegated other power of user 3"); + } + } + + function testDistributeBribes() public { + // Assume following situation: + // Sonic: U1: 100, U2: 200, U3: 300, U4: 400 + // Delegation on Sonic: U1 => U2, U3 => U2 + // Plasma: U1: 50, U2: 150, U4: 250 + // Avalanche: U1: 600, U3: 500 + // + // Suppose, U2 votes and receives 1000 bribes + // How to distribute the bribes between the users? + // + // Total number of votes of U2: (U1.sonic + U1.plasma + U1.avalanche) + (U2.sonic + U2.plasma) + (U3.sonic + U3.avalanche) + // (100 + 50 + 600) + (200 + 150) + (300 + 500) = 1900 + // U1: 1000 bribes * 750 / 1900 + // U2: 1000 bribes * 350 / 1900 + // U3: 1000 bribes * 800 / 1900 + + IDAO token = _createDAOInstance(); + + address user1 = makeAddr("user1"); + address user2 = makeAddr("user2"); + address user3 = makeAddr("user3"); + address user4 = makeAddr("user4"); + + // -------------------------- provide powers for user 1 and user 2 on main chain + deal(address(token), user1, 100e18); + deal(address(token), user2, 200e18); + deal(address(token), user3, 300e18); + deal(address(token), user4, 400e18); + + // -------------------------- set power on other chains for user 1 and user 2 + { + address[] memory users = new address[](4); + users[0] = user1; + users[1] = user2; + users[2] = user3; + users[3] = user4; + uint[] memory powers = new uint[](4); + powers[0] = 50e18 + 600e18; + powers[1] = 150e18; + powers[2] = 500e18; + powers[3] = 250e18; + + vm.prank(multisig); + token.setWhitelistedForOtherChainsPowers(user1, true); + + vm.prank(user1); + token.updateOtherChainsPowers(users, powers); + skip(1); // in next tx we should have different timestamp because it's used as an epoch counter inside token + } + + // -------------------------- users 1 and 3 delegate their powers to user 2 + vm.prank(user1); + token.setPowerDelegation(user2); + + vm.prank(user3); + token.setPowerDelegation(user2); + + // -------------------------- distribute bribes + Powers memory p1 = _getPowers(token, user1); + Powers memory p2 = _getPowers(token, user2); + Powers memory p3 = _getPowers(token, user3); + (, address[] memory delegators) = token.delegates(user2); + + // _showPowers(p1); + // _showPowers(p2); + // _showPowers(p3); + + assertEq(delegators.length, 2, "delegators are users 1 and 3"); + assertEq(delegators[0], user1, "first delegator is user 1"); + assertEq(delegators[1], user3, "second delegator is user 3"); + + assertEq( + p2.localPower + p2.otherPower + p2.delegatedLocalPower + p2.delegatedOtherPower, + 1900e18, + "total power of user 2" + ); + assertEq(p1.localPower + p1.otherPower, 750e18, "total power of user 1"); + assertEq(p2.localPower + p2.otherPower, 350e18, "total power of user 2"); + assertEq(p3.localPower + p3.otherPower, 800e18, "total power of user 3"); + + assertEq(p2.delegatedLocalPower + p2.delegatedOtherPower, 1900e18 - 350e18, "delegated power of user 2"); + } + + function testSetNameSymbol() public { + IDAO token = _createDAOInstance(); + + assertEq(keccak256(bytes(IERC20Metadata(address(token)).name())), keccak256(bytes("Stability DAO")), "name"); + assertEq(keccak256(bytes(IERC20Metadata(address(token)).symbol())), keccak256(bytes("STBL_DAO")), "symbol"); + + vm.expectRevert(IControllable.NotOperator.selector); + vm.prank(address(2)); + token.setName("NewName"); + + vm.prank(multisig); + token.setName("NewName"); + + assertEq(keccak256(bytes(IERC20Metadata(address(token)).name())), keccak256(bytes("NewName")), "new name"); + + vm.expectRevert(IControllable.NotOperator.selector); + vm.prank(address(2)); + token.setSymbol("NewSymbol"); + + vm.prank(multisig); + token.setSymbol("NewSymbol"); + + assertEq(keccak256(bytes(IERC20Metadata(address(token)).symbol())), keccak256(bytes("NewSymbol")), "new symbol"); + } + + //endregion --------------------------------- Unit tests + + //region --------------------------------- Utils + function _getPowers(IDAO dao, address user) internal view returns (Powers memory p) { + (p.localPower, p.otherPower) = dao.getPowers(user); + (, address[] memory delegators) = dao.delegates(user); + for (uint i; i < delegators.length; ++i) { + (uint dLocalPower, uint dOtherPower) = dao.getPowers(delegators[i]); + p.delegatedLocalPower += dLocalPower; + p.delegatedOtherPower += dOtherPower; + } + return p; + } + + function _showPowers(Powers memory p) internal pure { + console.log("localPower", p.localPower); + console.log("otherPower", p.otherPower); + console.log("delegatedLocalPower", p.delegatedLocalPower); + console.log("delegatedOtherPower", p.delegatedOtherPower); + } + + function _updateConfig( + IDAO token, + address user, + IDAO.DaoParams memory p2 + ) internal returns (IDAO.DaoParams memory dest) { + uint snapshot = vm.snapshotState(); + vm.prank(user); + token.updateConfig(p2); + dest = token.config(); + + vm.revertToState(snapshot); + return dest; + } + + function _createDAOInstance() internal returns (IDAO) { + IDAO.DaoParams memory p = IDAO.DaoParams({ + minimalPower: 4000e18, + exitPenalty: 80_00, + quorum: 15_000, + proposalThreshold: 25_000, + powerAllocationDelay: 86400 + }); + return _createDAOInstance(p); + } + + function _createDAOInstance(IDAO.DaoParams memory p) internal returns (IDAO) { + Proxy proxy = new Proxy(); + proxy.initProxy(address(new DAO())); + IDAO token = IDAO(address(proxy)); + token.initialize( + SonicConstantsLib.PLATFORM, + SonicConstantsLib.TOKEN_STBL, + SonicConstantsLib.XSTBL_XSTAKING, + p, + "Stability DAO", + "STBL_DAO" + ); + return token; + } + + function _upgradePlatform() internal { + rewind(1 days); + + IPlatform platform = IPlatform(SonicConstantsLib.PLATFORM); + + address[] memory proxies = new address[](1); + address[] memory implementations = new address[](1); + + proxies[0] = SonicConstantsLib.PLATFORM; + + implementations[0] = address(new Platform()); + + vm.startPrank(platform.multisig()); + platform.announcePlatformUpgrade("2025.08.21-alpha", proxies, implementations); + + skip(1 days); + platform.upgrade(); + vm.stopPrank(); + } + //endregion --------------------------------- Utils +} diff --git a/test/tokenomics/RecoveryRelayer.Plasma.t.sol b/test/tokenomics/RecoveryRelayer.Plasma.t.sol new file mode 100644 index 00000000..f68dc969 --- /dev/null +++ b/test/tokenomics/RecoveryRelayer.Plasma.t.sol @@ -0,0 +1,252 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import {RevenueRouter} from "../../src/tokenomics/RevenueRouter.sol"; +import {IControllable} from "../../src/interfaces/IControllable.sol"; +import {IPlatform} from "../../src/interfaces/IPlatform.sol"; +import {IRecoveryRelayer} from "../../src/interfaces/IRecoveryRelayer.sol"; +import {IRevenueRouter} from "../../src/interfaces/IRevenueRouter.sol"; +import {IStrategy} from "../../src/interfaces/IStrategy.sol"; +import {PlasmaConstantsLib} from "../../chains/plasma/PlasmaConstantsLib.sol"; +import {Proxy} from "../../src/core/proxy/Proxy.sol"; +import {RecoveryRelayerLib} from "../../src/tokenomics/libs/RecoveryRelayerLib.sol"; +import {RecoveryRelayer} from "../../src/tokenomics/RecoveryRelayer.sol"; +import {Test} from "forge-std/Test.sol"; +// import {console} from "forge-std/console.sol"; + +contract RecoveryRelayerPlasmaTest is Test { + uint public constant FORK_BLOCK = 8339817; // Dec-9-2025 08:54:48 UTC + address internal multisig; + + address public constant AMF_STRATEGY = 0x5AC5b2740F77200CCe6562795cFcf4c3c2aC3745; + + constructor() { + vm.selectFork(vm.createFork(vm.envString("PLASMA_RPC_URL"), FORK_BLOCK)); + multisig = IPlatform(PlasmaConstantsLib.PLATFORM).multisig(); + } + + //region --------------------------------- Unit tests + function testRecoveryStorageLocation() public pure { + assertEq( + keccak256(abi.encode(uint(keccak256("erc7201:stability.RecoveryRelayer")) - 1)) & ~bytes32(uint(0xff)), + RecoveryRelayerLib._RECOVERY_RELAYER_STORAGE_LOCATION, + "_RECOVERY_RELAYER_STORAGE_LOCATION" + ); + } + + function testSetThreshold() public { + RecoveryRelayer recoveryRelayer = createRecoveryRelayerInstance(); + + address[] memory assets = new address[](2); + assets[0] = PlasmaConstantsLib.TOKEN_USDT0; + assets[1] = PlasmaConstantsLib.TOKEN_WXPL; + + uint[] memory thresholds = new uint[](2); + thresholds[0] = 1e6; // usdt + thresholds[1] = 1e18; // wxpl + + assertEq(recoveryRelayer.threshold(assets[0]), 0, "usdt threshold is zero by default"); + assertEq(recoveryRelayer.threshold(assets[1]), 0, "wxpl threshold is zero by default"); + + vm.expectRevert(IControllable.NotOperator.selector); + vm.prank(address(this)); + recoveryRelayer.setThresholds(assets, thresholds); + + vm.prank(multisig); + recoveryRelayer.setThresholds(assets, thresholds); + + assertEq(recoveryRelayer.threshold(assets[0]), thresholds[0], "usdt threshold 1"); + assertEq(recoveryRelayer.threshold(assets[1]), thresholds[1], "wxpl threshold 1"); + + thresholds[0] = 2e6; // usdt + thresholds[1] = 0; // wxpl + + vm.prank(multisig); + recoveryRelayer.setThresholds(assets, thresholds); + + assertEq(recoveryRelayer.threshold(assets[0]), thresholds[0], "usdt threshold 2"); + assertEq(recoveryRelayer.threshold(assets[1]), thresholds[1], "wxpl threshold 2"); + } + + function testChangeWhitelist() public { + RecoveryRelayer recoveryRelayer = createRecoveryRelayerInstance(); + + address operator1 = makeAddr("operator1"); + address operator2 = makeAddr("operator2"); + + assertEq(recoveryRelayer.whitelisted(multisig), true, "multisig is whitelisted by default"); + assertEq(recoveryRelayer.whitelisted(operator1), false, "operator1 is not whitelisted by default"); + assertEq(recoveryRelayer.whitelisted(operator2), false, "operator2 is not whitelisted by default"); + + vm.expectRevert(IControllable.NotOperator.selector); + vm.prank(address(this)); + recoveryRelayer.changeWhitelist(operator1, true); + + vm.prank(multisig); + recoveryRelayer.changeWhitelist(operator1, true); + + assertEq(recoveryRelayer.whitelisted(operator1), true, "operator1 is whitelisted"); + assertEq(recoveryRelayer.whitelisted(operator2), false, "operator2 is not whitelisted"); + + vm.prank(multisig); + recoveryRelayer.changeWhitelist(operator2, true); + + assertEq(recoveryRelayer.whitelisted(operator2), true, "operator2 is whitelisted"); + + vm.prank(multisig); + recoveryRelayer.changeWhitelist(operator1, false); + + assertEq(recoveryRelayer.whitelisted(operator1), false, "operator1 is not whitelisted"); + assertEq(recoveryRelayer.whitelisted(operator2), true, "operator2 is whitelisted"); + } + + function testRegisterAssetsBadPaths() public { + RecoveryRelayer recoveryRelayer = createRecoveryRelayerInstance(); + + address[] memory tokens = new address[](2); + tokens[0] = PlasmaConstantsLib.TOKEN_USDT0; + tokens[1] = PlasmaConstantsLib.TOKEN_WXPL; + + assertEq(recoveryRelayer.isTokenRegistered(tokens[0]), false, "usdt not registered"); + assertEq(recoveryRelayer.isTokenRegistered(tokens[1]), false, "wxpl not registered"); + + vm.expectRevert(RecoveryRelayerLib.NotWhitelisted.selector); + vm.prank(address(this)); + recoveryRelayer.registerAssets(tokens); + + vm.prank(multisig); + recoveryRelayer.registerAssets(tokens); + + assertEq(recoveryRelayer.isTokenRegistered(tokens[0]), true, "usdt is registered"); + assertEq(recoveryRelayer.isTokenRegistered(tokens[1]), true, "wxpl is registered"); + + tokens[0] = PlasmaConstantsLib.TOKEN_USDE; + tokens[1] = PlasmaConstantsLib.TOKEN_USDT0; + + vm.prank(multisig); + recoveryRelayer.changeWhitelist(address(this), true); + + vm.prank(address(this)); + recoveryRelayer.registerAssets(tokens); + + assertEq(recoveryRelayer.isTokenRegistered(tokens[0]), true, "usde is registered"); + assertEq(recoveryRelayer.isTokenRegistered(tokens[1]), true, "usdc is registered"); + } + + function testGetListTokensToSwap() public { + RecoveryRelayer recoveryRelayer = createRecoveryRelayerInstance(); + + address[] memory tokens = new address[](3); + tokens[0] = PlasmaConstantsLib.TOKEN_USDT0; + tokens[1] = PlasmaConstantsLib.TOKEN_WXPL; + tokens[2] = PlasmaConstantsLib.TOKEN_USDE; + + vm.prank(multisig); + recoveryRelayer.registerAssets(tokens); + + address[] memory list = recoveryRelayer.getListTokensToSwap(); + assertEq(list.length, 0, "no tokens to swap"); + + // ------------------------- Put some assets on balance of Recovery + deal(PlasmaConstantsLib.TOKEN_USDT0, address(recoveryRelayer), 1e6); + deal(PlasmaConstantsLib.TOKEN_USDE, address(recoveryRelayer), 2e6); + + list = recoveryRelayer.getListTokensToSwap(); + assertEq(list.length, 2, "2 tokens to swap A"); + assertEq(list[0], PlasmaConstantsLib.TOKEN_USDT0, "token 0 is usdc A"); + assertEq(list[1], PlasmaConstantsLib.TOKEN_USDE, "token 1 is usdt A"); + + // ------------------------- Set high threshold for USDT + address[] memory assets = new address[](1); + assets[0] = PlasmaConstantsLib.TOKEN_USDT0; + + uint[] memory thresholds = new uint[](1); + thresholds[0] = 1e6; // usdt + + vm.prank(multisig); + recoveryRelayer.setThresholds(assets, thresholds); + + list = recoveryRelayer.getListTokensToSwap(); + assertEq(list.length, 1, "1 token to swap B"); + assertEq(list[0], PlasmaConstantsLib.TOKEN_USDE, "token 0 is usde B"); + + // ------------------------- Tests for auxiliary getListRegisteredTokens + list = recoveryRelayer.getListRegisteredTokens(); + assertEq(list.length, 3); + } + + //endregion --------------------------------- Unit tests + + /// @notice todo setup bridge on Plasma + function testUpgrade() internal { + // ---------------------- Setup RecoveryRelayer in the platform + { + Proxy proxy = new Proxy(); + address implementation = address(new RecoveryRelayer()); + proxy.initProxy(implementation); + + vm.prank(multisig); + IPlatform(PlasmaConstantsLib.PLATFORM).setupRecovery(address(proxy)); + } + + _upgradeRevenueRouter(); + + // ---------------------- Set up revenue router + IRevenueRouter revenueRouter = IRevenueRouter(IPlatform(PlasmaConstantsLib.PLATFORM).revenueRouter()); + + vm.prank(multisig); + revenueRouter.setXShare(100_000); // no transfers to treasury + + // IFactory factory = IFactory(IPlatform(PlasmaConstantsLib.PLATFORM).factory()); + // IFactory.Farm memory farm = factory.farm(0); + // console.log(farm.strategyLogicId); // Aave Merkl Farm + // + // address[] memory vaults = factory.deployedVaults(); + // for (uint i; i < vaults.length; ++i) { + // console.log("Vault:", vaults[i]); + // } + + // ---------------------- emulate merkl rewards + address vault = IStrategy(AMF_STRATEGY).vault(); + deal(PlasmaConstantsLib.TOKEN_WXPL, AMF_STRATEGY, 1e18); + + // ---------------------- hardwork + vm.prank(vault); + IStrategy(AMF_STRATEGY).doHardWork(); + + vm.prank(multisig); + revenueRouter.processAccumulatedAssets(1); + + // ---------------------- RecoveryRelayer receives 20% + address[] memory tokens = + IRecoveryRelayer(IPlatform(PlasmaConstantsLib.PLATFORM).recovery()).getListRegisteredTokens(); + assertNotEq(tokens.length, 0, "RecoveryRelayer has registered tokens"); + } + + //region --------------------------------- Utils + + function createRecoveryRelayerInstance() internal returns (RecoveryRelayer) { + Proxy proxy = new Proxy(); + proxy.initProxy(address(new RecoveryRelayer())); + RecoveryRelayer recovery = RecoveryRelayer(address(proxy)); + recovery.initialize(PlasmaConstantsLib.PLATFORM); + + return recovery; + } + + function _upgradeRevenueRouter() internal { + address revenueRouter = IPlatform(PlasmaConstantsLib.PLATFORM).revenueRouter(); + + address[] memory proxies = new address[](1); + proxies[0] = address(revenueRouter); + address[] memory implementations = new address[](1); + implementations[0] = address(new RevenueRouter()); + vm.startPrank(multisig); + IPlatform(PlasmaConstantsLib.PLATFORM).announcePlatformUpgrade("2025.12.0-alpha", proxies, implementations); + skip(18 hours); + IPlatform(PlasmaConstantsLib.PLATFORM).upgrade(); + vm.stopPrank(); + rewind(17 hours); + } + //endregion --------------------------------- Utils +} diff --git a/test/tokenomics/RevenueRouter.Sonic.t.sol b/test/tokenomics/RevenueRouter.Sonic.t.sol index dac51a9c..bb94dea5 100644 --- a/test/tokenomics/RevenueRouter.Sonic.t.sol +++ b/test/tokenomics/RevenueRouter.Sonic.t.sol @@ -5,21 +5,21 @@ import {Test} from "forge-std/Test.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {Proxy} from "../../src/core/proxy/Proxy.sol"; import {XStaking} from "../../src/tokenomics/XStaking.sol"; -import {XSTBL} from "../../src/tokenomics/XSTBL.sol"; +import {XToken} from "../../src/tokenomics/XToken.sol"; import {RevenueRouter} from "../../src/tokenomics/RevenueRouter.sol"; import {FeeTreasury} from "../../src/tokenomics/FeeTreasury.sol"; import {SonicConstantsLib} from "../../chains/sonic/SonicConstantsLib.sol"; import {IPlatform} from "../../src/interfaces/IPlatform.sol"; import {IRevenueRouter} from "../../src/interfaces/IRevenueRouter.sol"; -import {IXSTBL} from "../../src/interfaces/IXSTBL.sol"; import {IXStaking} from "../../src/interfaces/IXStaking.sol"; +import {IXToken} from "../../src/interfaces/IXToken.sol"; import {Platform} from "../../src/core/Platform.sol"; contract RevenueRouterTestSonic is Test { address public constant PLATFORM = SonicConstantsLib.PLATFORM; address public constant STBL = SonicConstantsLib.TOKEN_STBL; address public multisig; - IXSTBL public xStbl; + IXToken public xToken; IXStaking public xStaking; IRevenueRouter public revenueRouter; address public feeTreasury; @@ -33,8 +33,8 @@ contract RevenueRouterTestSonic is Test { _upgradePlatform(); } - function test_RevenueRouter_xStbl_feeTreasury() public { - _deployWithXSTBLandFeeTreasury(); + function test_RevenueRouter_xToken_feeTreasury() public { + _deployWithXTokenLandFeeTreasury(); deal(SonicConstantsLib.TOKEN_STBL, address(this), 1e10); IERC20(SonicConstantsLib.TOKEN_STBL).approve(address(revenueRouter), 1e10); @@ -53,16 +53,16 @@ contract RevenueRouterTestSonic is Test { //assertGt(pendingRevenue, 1e18); deal(SonicConstantsLib.TOKEN_STBL, address(this), 1e18); - IERC20(SonicConstantsLib.TOKEN_STBL).approve(address(xStbl), 1e18); - xStbl.enter(1e18); - IERC20(address(xStbl)).approve(address(xStaking), 1e18); + IERC20(SonicConstantsLib.TOKEN_STBL).approve(address(xToken), 1e18); + xToken.enter(1e18); + IERC20(address(xToken)).approve(address(xStaking), 1e18); xStaking.deposit(1e18); deal(SonicConstantsLib.TOKEN_STBL, address(1), 1e18); vm.startPrank(address(1)); - IERC20(SonicConstantsLib.TOKEN_STBL).approve(address(xStbl), 1e18); - xStbl.enter(1e18); - xStbl.exit(1e18); + IERC20(SonicConstantsLib.TOKEN_STBL).approve(address(xToken), 1e18); + xToken.enter(1e18); + xToken.exit(1e18); vm.stopPrank(); vm.expectRevert(); @@ -113,29 +113,30 @@ contract RevenueRouterTestSonic is Test { } function testAddresses() public { - _deployWithXSTBLandFeeTreasury(); + _deployWithXTokenLandFeeTreasury(); address[] memory addresses = revenueRouter.addresses(); assertEq(addresses[0], address(STBL)); - assertEq(addresses[1], address(xStbl)); + assertEq(addresses[1], address(xToken)); assertEq(addresses[2], address(xStaking)); assertEq(addresses[3], address(feeTreasury)); } - function _deployWithXSTBLandFeeTreasury() internal { + function _deployWithXTokenLandFeeTreasury() internal { Proxy xStakingProxy = new Proxy(); xStakingProxy.initProxy(address(new XStaking())); - Proxy xSTBLProxy = new Proxy(); - xSTBLProxy.initProxy(address(new XSTBL())); + Proxy xTokenProxy = new Proxy(); + xTokenProxy.initProxy(address(new XToken())); Proxy revenueRouterProxy = new Proxy(); revenueRouterProxy.initProxy(address(new RevenueRouter())); Proxy feeTreasuryProxy = new Proxy(); feeTreasuryProxy.initProxy(address(new FeeTreasury())); FeeTreasury(address(feeTreasuryProxy)).initialize(PLATFORM, IPlatform(PLATFORM).multisig()); - XStaking(address(xStakingProxy)).initialize(PLATFORM, address(xSTBLProxy)); - XSTBL(address(xSTBLProxy)).initialize(PLATFORM, STBL, address(xStakingProxy), address(revenueRouterProxy)); - RevenueRouter(address(revenueRouterProxy)).initialize(PLATFORM, address(xSTBLProxy), address(feeTreasuryProxy)); - xStbl = IXSTBL(address(xSTBLProxy)); + XStaking(address(xStakingProxy)).initialize(PLATFORM, address(xTokenProxy)); + XToken(address(xTokenProxy)) + .initialize(PLATFORM, STBL, address(xStakingProxy), address(revenueRouterProxy), "xStability", "xSTBL"); + RevenueRouter(address(revenueRouterProxy)).initialize(PLATFORM, address(xTokenProxy), address(feeTreasuryProxy)); + xToken = IXToken(address(xTokenProxy)); xStaking = IXStaking(address(xStakingProxy)); revenueRouter = IRevenueRouter(address(revenueRouterProxy)); feeTreasury = address(feeTreasuryProxy); diff --git a/test/tokenomics/RevenueRouter.Upgrade.424.Plasma.t.sol b/test/tokenomics/RevenueRouter.Upgrade.424.Plasma.t.sol new file mode 100644 index 00000000..693a2653 --- /dev/null +++ b/test/tokenomics/RevenueRouter.Upgrade.424.Plasma.t.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import {PlasmaConstantsLib} from "../../chains/plasma/PlasmaConstantsLib.sol"; +import {IControllable} from "../../src/core/Platform.sol"; +import {IPlatform} from "../../src/interfaces/IPlatform.sol"; +import {RevenueRouter, IRevenueRouter} from "../../src/tokenomics/RevenueRouter.sol"; +import {Test} from "forge-std/Test.sol"; + +contract RevenueRouterUpgrade424TestPlasma is Test { + uint public constant FORK_BLOCK = 8339817; // Dec-9-2025 08:54:48 UTC + address public constant PLATFORM = PlasmaConstantsLib.PLATFORM; + address public multisig; + IRevenueRouter public revenueRouter; + + constructor() { + vm.selectFork(vm.createFork(vm.envString("PLASMA_RPC_URL"), FORK_BLOCK)); + revenueRouter = IRevenueRouter(IPlatform(PLATFORM).revenueRouter()); + multisig = IPlatform(PLATFORM).multisig(); + + _upgradeRevenueRouter(); + } + + function testSetAddresses() public { + // Addresses of main-token, xToken, xStaking and feeTreasure token + address[] memory addr = revenueRouter.addresses(); + addr[0] = address(0x1); + addr[1] = address(0x2); + addr[2] = address(0x3); + addr[3] = address(0x4); + + vm.expectRevert(IControllable.NotGovernanceAndNotMultisig.selector); + vm.prank(makeAddr("not multisig")); + revenueRouter.setAddresses(addr); + + vm.prank(multisig); + revenueRouter.setAddresses(addr); + + address[] memory addrAfter = revenueRouter.addresses(); + assertEq(addr[0], addrAfter[0], "main-token address mismatch"); + assertEq(addr[1], addrAfter[1], "xToken address mismatch"); + assertEq(addr[2], addrAfter[2], "xStaking address mismatch"); + assertEq(addr[3], addrAfter[3], "feeTreasure address mismatch"); + } + + function testXShare() public { + uint xShareBefore = revenueRouter.xShare(); + assertNotEq(xShareBefore, 100_000, "xShare before upgrade mismatch"); + + vm.expectRevert(IControllable.NotGovernanceAndNotMultisig.selector); + vm.prank(makeAddr("not multisig")); + revenueRouter.setXShare(100_000); + + vm.prank(multisig); + revenueRouter.setXShare(100_000); + + assertEq(revenueRouter.xShare(), 100_000, "xShare after upgrade mismatch"); + } + + function _upgradeRevenueRouter() internal { + address[] memory proxies = new address[](1); + proxies[0] = address(revenueRouter); + address[] memory implementations = new address[](1); + implementations[0] = address(new RevenueRouter()); + vm.startPrank(multisig); + IPlatform(PLATFORM).announcePlatformUpgrade("2025.12.0-alpha", proxies, implementations); + skip(18 hours); + IPlatform(PLATFORM).upgrade(); + vm.stopPrank(); + rewind(17 hours); + } +} diff --git a/test/tokenomics/StabilityDAO.t.sol b/test/tokenomics/StabilityDAO.t.sol deleted file mode 100644 index 5b2625c1..00000000 --- a/test/tokenomics/StabilityDAO.t.sol +++ /dev/null @@ -1,341 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.28; - -// import {console} from "forge-std/console.sol"; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {IPlatform} from "../../src/interfaces/IPlatform.sol"; -import {IControllable} from "../../src/interfaces/IControllable.sol"; -import {IStabilityDAO} from "../../src/interfaces/IStabilityDAO.sol"; -import {Proxy} from "../../src/core/proxy/Proxy.sol"; -import {SonicConstantsLib} from "../../chains/sonic/SonicConstantsLib.sol"; -import {Test} from "forge-std/Test.sol"; -import {StabilityDAO} from "../../src/tokenomics/StabilityDAO.sol"; -import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import {Platform} from "../../src/core/Platform.sol"; - -contract StabilityDAOSonicTest is Test { - using SafeERC20 for IERC20; - - uint public constant FORK_BLOCK = 47854805; // Sep-23-2025 04:02:39 AM +UTC - address internal multisig; - - constructor() { - vm.selectFork(vm.createFork(vm.envString("SONIC_RPC_URL"), FORK_BLOCK)); - multisig = IPlatform(SonicConstantsLib.PLATFORM).multisig(); - _upgradePlatform(); - - // console.logBytes32( - // keccak256(abi.encode(uint(keccak256("erc7201:stability.StabilityDAO")) - 1)) & ~bytes32(uint(0xff)) - // ); - } - - //region --------------------------------- Unit tests - - function testInitializeAndView() public { - IStabilityDAO.DaoParams memory p = IStabilityDAO.DaoParams({ - minimalPower: 4000e18, - exitPenalty: 50_00, - quorum: 20_000, - proposalThreshold: 10_000, - powerAllocationDelay: 86400 - }); - - Proxy proxy = new Proxy(); - proxy.initProxy(address(new StabilityDAO())); - - IStabilityDAO token = IStabilityDAO(address(proxy)); - token.initialize(SonicConstantsLib.PLATFORM, address(1), address(2), p); - - assertEq(token.xStbl(), address(1)); - assertEq(token.xStaking(), address(2)); - assertEq(token.name(), "Stability DAO"); - assertEq(token.symbol(), "STBL_DAO"); - assertEq(token.decimals(), 18); - - assertEq(token.minimalPower(), p.minimalPower); - assertEq(token.exitPenalty(), p.exitPenalty); - assertEq(token.proposalThreshold(), p.proposalThreshold); - assertEq(token.powerAllocationDelay(), p.powerAllocationDelay); - assertEq(token.quorum(), p.quorum); - } - - function testMintBurn() public { - address governance = IPlatform(SonicConstantsLib.PLATFORM).governance(); - IStabilityDAO token = _createStabilityDAOInstance(); - - vm.prank(address(0x123)); - vm.expectRevert(IControllable.IncorrectMsgSender.selector); - token.mint(address(0x123), 1e18); - - vm.prank(address(0x123)); - vm.expectRevert(IControllable.IncorrectMsgSender.selector); - token.burn(address(0x123), 1e18); - - vm.prank(multisig); - vm.expectRevert(IControllable.IncorrectMsgSender.selector); - token.mint(address(0x123), 1e18); - - vm.prank(multisig); - vm.expectRevert(IControllable.IncorrectMsgSender.selector); - token.burn(address(0x123), 1e18); - - vm.prank(governance); - vm.expectRevert(IControllable.IncorrectMsgSender.selector); - token.mint(address(0x123), 1e18); - - vm.prank(governance); - vm.expectRevert(IControllable.IncorrectMsgSender.selector); - token.burn(address(0x123), 1e18); - - vm.prank(token.xStaking()); - token.mint(address(0x123), 1e18); - assertEq(token.balanceOf(address(0x123)), 1e18); - - vm.prank(token.xStaking()); - token.burn(address(0x123), 0.5e18); - assertEq(token.balanceOf(address(0x123)), 0.5e18); - - vm.prank(token.xStaking()); - token.burn(address(0x123), 0.5e18); - assertEq(token.balanceOf(address(0x123)), 0); - } - - function testUpdateConfig() public { - IStabilityDAO.DaoParams memory p1 = IStabilityDAO.DaoParams({ - minimalPower: 4000e18, - exitPenalty: 50_00, // 50% - quorum: 20_000, // 20% - proposalThreshold: 10_000, // 10% - powerAllocationDelay: 86400 - }); - - IStabilityDAO.DaoParams memory p2 = IStabilityDAO.DaoParams({ - minimalPower: 5000e18, - exitPenalty: 80_00, // 80% - quorum: 35_000, // 35% - proposalThreshold: 20_000, // 20% - powerAllocationDelay: 172800 - }); - - IStabilityDAO token = _createStabilityDAOInstance(p1); - - vm.prank(multisig); - IPlatform(SonicConstantsLib.PLATFORM).setupStabilityDAO(address(token)); - - IStabilityDAO.DaoParams memory config = token.config(); - assertEq(config.minimalPower, p1.minimalPower, "minimalPower"); - assertEq(config.exitPenalty, p1.exitPenalty, "exitPenalty"); - assertEq(config.proposalThreshold, p1.proposalThreshold, "proposalThreshold"); - assertEq(config.powerAllocationDelay, p1.powerAllocationDelay, "powerAllocationDelay"); - assertEq(config.quorum, p1.quorum, "quorum"); - - vm.prank(address(0x123)); - vm.expectRevert(IControllable.NotGovernanceAndNotMultisig.selector); - token.updateConfig(p2); - - vm.prank(token.xStaking()); - vm.expectRevert(IControllable.NotGovernanceAndNotMultisig.selector); - token.updateConfig(p2); - - config = _updateConfig(token, multisig, p2); - - assertEq(config.minimalPower, p2.minimalPower, "minimalPower2"); - assertEq(config.exitPenalty, p2.exitPenalty, "exitPenalty2"); - assertEq(config.proposalThreshold, p2.proposalThreshold, "proposalThreshold2"); - assertEq(config.powerAllocationDelay, p2.powerAllocationDelay, "powerAllocationDelay2"); - assertEq(config.quorum, p2.quorum, "quorum2"); - - config = _updateConfig(token, IPlatform(SonicConstantsLib.PLATFORM).governance(), p2); - - assertEq(config.minimalPower, p2.minimalPower, "minimalPower3"); - assertEq(config.exitPenalty, p2.exitPenalty, "exitPenalty3"); - assertEq(config.proposalThreshold, p2.proposalThreshold, "proposalThreshold3"); - assertEq(config.powerAllocationDelay, p2.powerAllocationDelay, "powerAllocationDelay3"); - assertEq(config.quorum, p2.quorum, "quorum3"); - } - - function testUpdateConfigBadPaths() public { - IStabilityDAO.DaoParams memory p1 = IStabilityDAO.DaoParams({ - minimalPower: 4000e18, - exitPenalty: 50_00, // 50% - quorum: 20_000, // 20% - proposalThreshold: 10_000, // 10% - powerAllocationDelay: 86400 - }); - IStabilityDAO token = _createStabilityDAOInstance(p1); - - p1.proposalThreshold = 100_000; // 100% - - vm.prank(multisig); - vm.expectRevert(StabilityDAO.WrongValue.selector); - token.updateConfig(p1); - - p1.proposalThreshold = 10_000; - p1.exitPenalty = 100_00; // 100% - - vm.prank(multisig); - vm.expectRevert(StabilityDAO.WrongValue.selector); - token.updateConfig(p1); - - p1.exitPenalty = 50_00; - p1.quorum = 100_000; // 100% - - vm.prank(multisig); - vm.expectRevert(StabilityDAO.WrongValue.selector); - token.updateConfig(p1); - } - - function testNonTransferable() public { - IStabilityDAO token = _createStabilityDAOInstance(); - - vm.prank(token.xStaking()); - token.mint(address(0x123), 1e18); - - vm.prank(address(0x123)); - vm.expectRevert(StabilityDAO.NonTransferable.selector); - // slither-disable-next-line erc20-unchecked-transfer - // forge-lint: disable-next-line(erc20-unchecked-transfer) - token.transfer(address(0x456), 1e18); - - vm.prank(address(0x123)); - token.approve(address(0x456), 1e18); - - vm.prank(address(0x456)); - vm.expectRevert(StabilityDAO.NonTransferable.selector); - // slither-disable-next-line erc20-unchecked-transfer - // forge-lint: disable-next-line(erc20-unchecked-transfer) - token.transferFrom(address(0x123), address(0x789), 1e18); - } - - function testSetPowerDelegation() public { - address user1 = address(1); - address user2 = address(2); - IStabilityDAO stabilityDao = _createStabilityDAOInstance(); - - // ---------------------------- Initial state - vm.prank(stabilityDao.xStaking()); - stabilityDao.mint(user1, 10_000e18); - - vm.prank(stabilityDao.xStaking()); - stabilityDao.mint(user2, 20_000e18); - - assertEq(stabilityDao.getVotes(user1), 10_000e18); - assertEq(stabilityDao.getVotes(user2), 20_000e18); - - (address delegatedTo, address[] memory delegates) = stabilityDao.delegates(user1); - assertEq(delegatedTo, address(0)); - assertEq(delegates.length, 0); - - // ---------------------------- User 1 delegates to User 2 - vm.prank(user1); - stabilityDao.setPowerDelegation(user2); - - vm.expectRevert(StabilityDAO.AlreadyDelegated.selector); - vm.prank(user1); - stabilityDao.setPowerDelegation(address(this)); - - assertEq(stabilityDao.getVotes(user1), 0); - assertEq(stabilityDao.getVotes(user2), 20_000e18 + 10_000e18); - - (delegatedTo, delegates) = stabilityDao.delegates(user1); - assertEq(delegatedTo, user2); - assertEq(delegates.length, 0); - - (delegatedTo, delegates) = stabilityDao.delegates(user2); - assertEq(delegatedTo, address(0)); - assertEq(delegates.length, 1); - assertEq(delegates[0], user1); - - // ---------------------------- User 2 delegates to User 1 - vm.prank(user2); - stabilityDao.setPowerDelegation(user1); - - assertEq(stabilityDao.getVotes(user1), 20_000e18); - assertEq(stabilityDao.getVotes(user2), 10_000e18); - - (delegatedTo, delegates) = stabilityDao.delegates(user1); - assertEq(delegatedTo, user2); - assertEq(delegates.length, 1); - assertEq(delegates[0], user2); - - (delegatedTo, delegates) = stabilityDao.delegates(user2); - assertEq(delegatedTo, user1); - assertEq(delegates.length, 1); - assertEq(delegates[0], user1); - - // ---------------------------- Both Users clear delegations - vm.prank(user1); - stabilityDao.setPowerDelegation(user1); - - vm.prank(user2); - stabilityDao.setPowerDelegation(address(0)); - - assertEq(stabilityDao.getVotes(user1), 10_000e18); - assertEq(stabilityDao.getVotes(user2), 20_000e18); - - (delegatedTo, delegates) = stabilityDao.delegates(user1); - assertEq(delegatedTo, address(0)); - assertEq(delegates.length, 0); - - (delegatedTo, delegates) = stabilityDao.delegates(user2); - assertEq(delegatedTo, address(0)); - assertEq(delegates.length, 0); - } - - //endregion --------------------------------- Unit tests - - //region --------------------------------- Utils - function _updateConfig( - IStabilityDAO token, - address user, - IStabilityDAO.DaoParams memory p2 - ) internal returns (IStabilityDAO.DaoParams memory dest) { - uint snapshot = vm.snapshotState(); - vm.prank(user); - token.updateConfig(p2); - dest = token.config(); - - vm.revertToState(snapshot); - return dest; - } - - function _createStabilityDAOInstance() internal returns (IStabilityDAO) { - IStabilityDAO.DaoParams memory p = IStabilityDAO.DaoParams({ - minimalPower: 4000e18, - exitPenalty: 80_00, - quorum: 15_000, - proposalThreshold: 25_000, - powerAllocationDelay: 86400 - }); - return _createStabilityDAOInstance(p); - } - - function _createStabilityDAOInstance(IStabilityDAO.DaoParams memory p) internal returns (IStabilityDAO) { - Proxy proxy = new Proxy(); - proxy.initProxy(address(new StabilityDAO())); - IStabilityDAO token = IStabilityDAO(address(proxy)); - token.initialize(SonicConstantsLib.PLATFORM, SonicConstantsLib.TOKEN_STBL, SonicConstantsLib.XSTBL_XSTAKING, p); - return token; - } - - function _upgradePlatform() internal { - rewind(1 days); - - IPlatform platform = IPlatform(SonicConstantsLib.PLATFORM); - - address[] memory proxies = new address[](1); - address[] memory implementations = new address[](1); - - proxies[0] = SonicConstantsLib.PLATFORM; - - implementations[0] = address(new Platform()); - - vm.startPrank(platform.multisig()); - platform.announcePlatformUpgrade("2025.08.21-alpha", proxies, implementations); - - skip(1 days); - platform.upgrade(); - vm.stopPrank(); - } - //endregion --------------------------------- Utils -} diff --git a/test/tokenomics/XSTBL.t.sol b/test/tokenomics/XSTBL.t.sol deleted file mode 100644 index cec56d73..00000000 --- a/test/tokenomics/XSTBL.t.sol +++ /dev/null @@ -1,207 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.28; - -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {Test} from "forge-std/Test.sol"; -import {MockSetup} from "../base/MockSetup.sol"; -import {Proxy} from "../../src/core/proxy/Proxy.sol"; -import {IXSTBL} from "../../src/interfaces/IXSTBL.sol"; -import {IXStaking} from "../../src/interfaces/IXStaking.sol"; -import {IStabilityDAO} from "../../src/interfaces/IStabilityDAO.sol"; -import {IControllable} from "../../src/interfaces/IControllable.sol"; -import {XStaking} from "../../src/tokenomics/XStaking.sol"; -import {XSTBL} from "../../src/tokenomics/XSTBL.sol"; -import {RevenueRouter} from "../../src/tokenomics/RevenueRouter.sol"; -import {FeeTreasury} from "../../src/tokenomics/FeeTreasury.sol"; -import {StabilityDAO} from "../../src/tokenomics/StabilityDAO.sol"; -// import {console} from "forge-std/console.sol"; - -contract XSTBLTest is Test, MockSetup { - address public stbl; - IXSTBL public xStbl; - IXStaking public xStaking; - - function setUp() public { - stbl = address(tokenA); - Proxy xStakingProxy = new Proxy(); - xStakingProxy.initProxy(address(new XStaking())); - Proxy xSTBLProxy = new Proxy(); - xSTBLProxy.initProxy(address(new XSTBL())); - Proxy revenueRouterProxy = new Proxy(); - revenueRouterProxy.initProxy(address(new RevenueRouter())); - Proxy feeTreasuryProxy = new Proxy(); - feeTreasuryProxy.initProxy(address(new FeeTreasury())); - FeeTreasury(address(feeTreasuryProxy)).initialize(address(platform), platform.multisig()); - XStaking(address(xStakingProxy)).initialize(address(platform), address(xSTBLProxy)); - XSTBL(address(xSTBLProxy)) - .initialize(address(platform), stbl, address(xStakingProxy), address(revenueRouterProxy)); - RevenueRouter(address(revenueRouterProxy)) - .initialize(address(platform), address(xSTBLProxy), address(feeTreasuryProxy)); - xStbl = IXSTBL(address(xSTBLProxy)); - xStaking = IXStaking(address(xStakingProxy)); - //console.logBytes32(keccak256(abi.encode(uint256(keccak256("erc7201:stability.XSTBL")) - 1)) & ~bytes32(uint256(0xff))); - } - - function test_transfer() public { - tokenA.mint(100e18); - IERC20(stbl).approve(address(xStbl), 100e18); - xStbl.enter(100e18); - - vm.expectRevert(abi.encodeWithSelector(IXSTBL.NOT_WHITELISTED.selector, address(this), address(1))); - /// forge-lint: disable-next-line - IERC20(address(xStbl)).transfer(address(1), 1e18); - - address[] memory exemptee = new address[](1); - exemptee[0] = address(this); - bool[] memory exempt = new bool[](2); - vm.expectRevert(abi.encodeWithSelector(IControllable.IncorrectArrayLength.selector)); - xStbl.setExemptionFrom(exemptee, exempt); - vm.expectRevert(abi.encodeWithSelector(IControllable.IncorrectArrayLength.selector)); - xStbl.setExemptionTo(exemptee, exempt); - vm.prank(address(101)); - vm.expectRevert(abi.encodeWithSelector(IControllable.NotGovernanceAndNotMultisig.selector)); - xStbl.setExemptionFrom(exemptee, exempt); - vm.prank(address(101)); - vm.expectRevert(abi.encodeWithSelector(IControllable.NotGovernanceAndNotMultisig.selector)); - xStbl.setExemptionTo(exemptee, exempt); - exempt = new bool[](1); - exempt[0] = true; - xStbl.setExemptionFrom(exemptee, exempt); - - /// forge-lint: disable-next-line - IERC20(address(xStbl)).transfer(address(1), 1e18); - - exempt[0] = false; - xStbl.setExemptionFrom(exemptee, exempt); - vm.expectRevert(abi.encodeWithSelector(IXSTBL.NOT_WHITELISTED.selector, address(this), address(1))); - /// forge-lint: disable-next-line - IERC20(address(xStbl)).transfer(address(1), 1e18); - - exemptee[0] = address(1); - exempt[0] = true; - xStbl.setExemptionTo(exemptee, exempt); - /// forge-lint: disable-next-line - IERC20(address(xStbl)).transfer(address(1), 1e18); - - vm.prank(address(1)); - vm.expectRevert(abi.encodeWithSelector(IXSTBL.NOT_WHITELISTED.selector, address(1), address(2))); - /// forge-lint: disable-next-line - IERC20(address(xStbl)).transfer(address(2), 1e18); - } - - function test_enter_exit() public { - tokenA.mint(100e18); - IERC20(stbl).approve(address(xStbl), 100e18); - - // enter - xStbl.enter(100e18); - assertEq(IERC20(address(xStbl)).balanceOf(address(this)), 100e18); - assertEq(IERC20(stbl).balanceOf(address(xStbl)), 100e18); - - // instant exit - xStbl.exit(50e18); - assertEq(IERC20(address(xStbl)).balanceOf(address(this)), 50e18); - assertEq(IERC20(stbl).balanceOf(address(this)), 25e18); - assertEq(IERC20(stbl).balanceOf(address(xStbl)), 75e18); - - // create vest - uint time = block.timestamp; - xStbl.createVest(30e18); - (uint amount, uint start, uint maxEnd) = xStbl.vestInfo(address(this), 0); - assertEq(amount, 30e18); - assertEq(start, time); - assertEq(maxEnd, time + xStbl.MAX_VEST()); - assertEq(xStbl.usersTotalVests(address(this)), 1); - assertEq(IERC20(address(xStbl)).balanceOf(address(this)), 20e18); - - // cancel vesting - vm.warp(time + 13 days); - xStbl.exitVest(0); - (amount,,) = xStbl.vestInfo(address(this), 0); - assertEq(amount, 0); - assertEq(IERC20(address(xStbl)).balanceOf(address(this)), 50e18); - assertEq(xStbl.pendingRebase(), 25e18); - - // exit vesting in progress - time = block.timestamp; - xStbl.createVest(30e18); - assertEq(xStbl.usersTotalVests(address(this)), 2); - vm.warp(time + 179 days); - xStbl.exitVest(1); - (amount,,) = xStbl.vestInfo(address(this), 1); - assertEq(amount, 0); - assertGt(IERC20(stbl).balanceOf(address(this)), 25e18 + 29e18); - assertLt(IERC20(stbl).balanceOf(address(this)), 25e18 + 30e18); - - // exit completed vesting - time = block.timestamp; - xStbl.createVest(20e18); - vm.warp(time + 200 days); - uint balanceWas = IERC20(stbl).balanceOf(address(this)); - xStbl.exitVest(2); - assertEq(IERC20(stbl).balanceOf(address(this)), balanceWas + 20e18); - } - - function test_reverts() public { - vm.expectRevert(); - xStbl.rebase(); - - vm.expectRevert(); - xStbl.enter(0); - - vm.expectRevert(); - xStbl.exit(0); - - vm.expectRevert(); - xStbl.createVest(0); - - vm.expectRevert(); - xStbl.exitVest(10); - } - - function testSlashingPenalty() public { - // --------------------- StabilityDAO is not initialized - assertEq(xStbl.SLASHING_PENALTY(), 50_00, "50% by default"); - - // --------------------- Set up StabilityDAO - IStabilityDAO daoToken = _createStabilityDAOInstance(); - platform.setupStabilityDAO(address(daoToken)); - - _setSlashingPenalty(daoToken, 80_00); - assertEq(xStbl.SLASHING_PENALTY(), 80_00, "80%"); - - _setSlashingPenalty(daoToken, 30_00); - assertEq(xStbl.SLASHING_PENALTY(), 30_00, "30%"); - - _setSlashingPenalty(daoToken, 0); - assertEq(xStbl.SLASHING_PENALTY(), 50_00, "DEFAULT_SLASHING_PENALTY"); - } - - // region --------------------- Helpers - function _setSlashingPenalty(IStabilityDAO daoToken, uint penalty_) internal { - address multisig = platform.multisig(); - - IStabilityDAO.DaoParams memory config = daoToken.config(); - config.exitPenalty = penalty_; - - vm.prank(multisig); - daoToken.updateConfig(config); - } - - function _createStabilityDAOInstance() internal returns (IStabilityDAO) { - IStabilityDAO.DaoParams memory p = IStabilityDAO.DaoParams({ - minimalPower: 4000e18, - exitPenalty: 50_00, - quorum: 20_00, - proposalThreshold: 10_00, - powerAllocationDelay: 86400 - }); - - Proxy proxy = new Proxy(); - proxy.initProxy(address(new StabilityDAO())); - IStabilityDAO token = IStabilityDAO(address(proxy)); - token.initialize(address(platform), address(xStbl), address(xStaking), p); - return token; - } - // endregion --------------------- Helpers -} diff --git a/test/tokenomics/XStaking.Upgrade.404.t.sol b/test/tokenomics/XStaking.Upgrade.404.t.sol index ab4e07b5..d6726a9e 100644 --- a/test/tokenomics/XStaking.Upgrade.404.t.sol +++ b/test/tokenomics/XStaking.Upgrade.404.t.sol @@ -7,11 +7,11 @@ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {SonicConstantsLib} from "../../chains/sonic/SonicConstantsLib.sol"; import {IPlatform} from "../../src/interfaces/IPlatform.sol"; import {Proxy} from "../../src/core/proxy/Proxy.sol"; -import {IStabilityDAO} from "../../src/interfaces/IStabilityDAO.sol"; -import {StabilityDAO} from "../../src/tokenomics/StabilityDAO.sol"; +import {IDAO} from "../../src/interfaces/IDAO.sol"; +import {DAO} from "../../src/tokenomics/DAO.sol"; import {XStaking} from "../../src/tokenomics/XStaking.sol"; -import {XSTBL} from "../../src/tokenomics/XSTBL.sol"; -import {IXSTBL} from "../../src/interfaces/IXSTBL.sol"; +import {XToken} from "../../src/tokenomics/XToken.sol"; +import {IXToken} from "../../src/interfaces/IXToken.sol"; import {IXStaking} from "../../src/interfaces/IXStaking.sol"; import {Platform} from "../../src/core/Platform.sol"; @@ -21,7 +21,7 @@ contract XStakingUpgrade404SonicTest is Test { address public multisig; IXStaking public xStaking; - IXSTBL public xStbl; + IXToken public xToken; address public constant USER1 = address(0x1001); address public constant USER2 = address(0x1002); @@ -32,11 +32,11 @@ contract XStakingUpgrade404SonicTest is Test { multisig = IPlatform(PLATFORM).multisig(); xStaking = IXStaking(SonicConstantsLib.XSTBL_XSTAKING); - xStbl = IXSTBL(SonicConstantsLib.TOKEN_XSTBL); + xToken = IXToken(SonicConstantsLib.TOKEN_XSTBL); } function testDepositWithdrawXStaking() public { - // ------------------------------- mint xSTBL and deposit to staking before upgrade + // ------------------------------- mint xToken and deposit to staking before upgrade _mintAndDepositToStaking(USER1, 5000e18); _mintAndDepositToStaking(USER2, 3000e18); @@ -46,14 +46,14 @@ contract XStakingUpgrade404SonicTest is Test { users[2] = USER3; // ------------------------------- Upgrade and sync - IStabilityDAO stabilityDao = _upgradeAndSetup(); + IDAO dao = _upgradeAndSetup(); vm.prank(multisig); - xStaking.syncStabilityDAOBalances(users); + xStaking.syncDAOBalances(users); - assertEq(stabilityDao.getVotes(USER1), 5000e18, "1: user 1 power"); - assertEq(stabilityDao.getVotes(USER2), 0, "1: user 2 power"); - assertEq(stabilityDao.getVotes(USER3), 0, "1: user 3 power"); + assertEq(dao.getVotes(USER1), 5000e18, "1: user 1 power"); + assertEq(dao.getVotes(USER2), 0, "1: user 2 power"); + assertEq(dao.getVotes(USER3), 0, "1: user 3 power"); assertEq(xStaking.balanceOf(USER1), 5000e18, "1: user 1 xStaking balance"); assertEq(xStaking.balanceOf(USER2), 3000e18, "1: user 2 xStaking balance"); @@ -66,9 +66,9 @@ contract XStakingUpgrade404SonicTest is Test { vm.prank(USER1); xStaking.withdraw(1000e18); - assertEq(stabilityDao.getVotes(USER1), 4000e18, "2: user 1 power"); - assertEq(stabilityDao.getVotes(USER2), 4000e18, "2: user 2 power"); - assertEq(stabilityDao.getVotes(USER3), 4000e18, "2: user 3 power"); + assertEq(dao.getVotes(USER1), 4000e18, "2: user 1 power"); + assertEq(dao.getVotes(USER2), 4000e18, "2: user 2 power"); + assertEq(dao.getVotes(USER3), 4000e18, "2: user 3 power"); assertEq(xStaking.balanceOf(USER1), 4000e18, "2: user 1 xStaking balance"); assertEq(xStaking.balanceOf(USER2), 4000e18, "2: user 2 xStaking balance"); @@ -81,9 +81,9 @@ contract XStakingUpgrade404SonicTest is Test { vm.prank(USER1); xStaking.withdraw(1000e18); - assertEq(stabilityDao.getVotes(USER1), 0, "3: user 1 power"); - assertEq(stabilityDao.getVotes(USER2), 5000e18, "3: user 2 power"); - assertEq(stabilityDao.getVotes(USER3), 5000e18, "3: user 3 power"); + assertEq(dao.getVotes(USER1), 0, "3: user 1 power"); + assertEq(dao.getVotes(USER2), 5000e18, "3: user 2 power"); + assertEq(dao.getVotes(USER3), 5000e18, "3: user 3 power"); assertEq(xStaking.balanceOf(USER1), 3000e18, "3: user 1 xStaking balance"); assertEq(xStaking.balanceOf(USER2), 5000e18, "3: user 2 xStaking balance"); @@ -98,9 +98,9 @@ contract XStakingUpgrade404SonicTest is Test { vm.prank(USER3); xStaking.withdraw(1500e18); - assertEq(stabilityDao.getVotes(USER1), 8000e18, "4: user 1 power"); - assertEq(stabilityDao.getVotes(USER2), 0, "4: user 2 power"); - assertEq(stabilityDao.getVotes(USER3), 0, "4: user 3 power"); + assertEq(dao.getVotes(USER1), 8000e18, "4: user 1 power"); + assertEq(dao.getVotes(USER2), 0, "4: user 2 power"); + assertEq(dao.getVotes(USER3), 0, "4: user 3 power"); assertEq(xStaking.balanceOf(USER1), 8000e18, "4: user 1 xStaking balance"); assertEq(xStaking.balanceOf(USER2), 0, "4: user 2 xStaking balance"); @@ -108,7 +108,7 @@ contract XStakingUpgrade404SonicTest is Test { } function testDelegation() public { - // ------------------------------- mint xSTBL and deposit to staking before upgrade + // ------------------------------- mint xToken and deposit to staking before upgrade address[] memory users = new address[](3); users[0] = USER1; users[1] = USER2; @@ -119,11 +119,11 @@ contract XStakingUpgrade404SonicTest is Test { uint balance3 = 3003e18; // ------------------------------- Upgrade and sync - IStabilityDAO stabilityDao = _upgradeAndSetup(); - assertEq(stabilityDao.minimalPower(), 4000e18, "initial minimal power is very high"); + IDAO dao = _upgradeAndSetup(); + assertEq(dao.minimalPower(), 4000e18, "initial minimal power is very high"); vm.prank(multisig); - xStaking.syncStabilityDAOBalances(users); + xStaking.syncDAOBalances(users); // ------------------------------- Deposit 1 _mintAndDepositToStaking(USER1, balance1); @@ -134,21 +134,21 @@ contract XStakingUpgrade404SonicTest is Test { assertEq(xStaking.balanceOf(USER2), balance2, "1: user 2 xStaking balance"); assertEq(xStaking.balanceOf(USER3), balance3, "1: user 3 xStaking balance"); - assertEq(stabilityDao.getVotes(USER1), 0, "1: user 1 power"); - assertEq(stabilityDao.getVotes(USER2), 0, "1: user 2 power"); - assertEq(stabilityDao.getVotes(USER3), 0, "1: user 3 power"); + assertEq(dao.getVotes(USER1), 0, "1: user 1 power"); + assertEq(dao.getVotes(USER2), 0, "1: user 2 power"); + assertEq(dao.getVotes(USER3), 0, "1: user 3 power"); // ------------------------------- Users 1 and 3 delegate to user 2 vm.prank(USER1); - stabilityDao.setPowerDelegation(USER2); + dao.setPowerDelegation(USER2); vm.prank(USER3); - stabilityDao.setPowerDelegation(USER2); + dao.setPowerDelegation(USER2); // ------------------------------- Threshold is too high, users don't have any power - assertEq(stabilityDao.getVotes(USER1), 0, "2: user 1 power"); - assertEq(stabilityDao.getVotes(USER2), 0, "2: user 2 power"); - assertEq(stabilityDao.getVotes(USER3), 0, "2: user 3 power"); + assertEq(dao.getVotes(USER1), 0, "2: user 1 power"); + assertEq(dao.getVotes(USER2), 0, "2: user 2 power"); + assertEq(dao.getVotes(USER3), 0, "2: user 3 power"); assertEq(xStaking.balanceOf(USER1), balance1, "2: user 1 xStaking balance"); assertEq(xStaking.balanceOf(USER2), balance2, "2: user 2 xStaking balance"); @@ -157,12 +157,12 @@ contract XStakingUpgrade404SonicTest is Test { // ------------------------------- Reduce threshold 4000 => 1000 and sync _updateMinimalPower(1000e18); vm.prank(multisig); - xStaking.syncStabilityDAOBalances(users); + xStaking.syncDAOBalances(users); // ------------------------------- Now user 2 has all power because users 1 and 3 have delegated him their powers - assertEq(stabilityDao.getVotes(USER1), 0, "2: user 1 power"); - assertEq(stabilityDao.getVotes(USER2), balance2 + balance1 + balance3, "2: user 2 power"); - assertEq(stabilityDao.getVotes(USER3), 0, "2: user 3 power"); + assertEq(dao.getVotes(USER1), 0, "2: user 1 power"); + assertEq(dao.getVotes(USER2), balance2 + balance1 + balance3, "2: user 2 power"); + assertEq(dao.getVotes(USER3), 0, "2: user 3 power"); assertEq(xStaking.balanceOf(USER1), balance1, "2: user 1 xStaking balance"); assertEq(xStaking.balanceOf(USER2), balance2, "2: user 2 xStaking balance"); @@ -170,14 +170,14 @@ contract XStakingUpgrade404SonicTest is Test { // ------------------------------- Clear delegation vm.prank(USER1); - stabilityDao.setPowerDelegation(USER1); + dao.setPowerDelegation(USER1); vm.prank(USER3); - stabilityDao.setPowerDelegation(USER3); + dao.setPowerDelegation(USER3); - assertEq(stabilityDao.getVotes(USER1), balance1, "4: user 1 power"); - assertEq(stabilityDao.getVotes(USER2), balance2, "4: user 2 power"); - assertEq(stabilityDao.getVotes(USER3), balance3, "4: user 3 power"); + assertEq(dao.getVotes(USER1), balance1, "4: user 1 power"); + assertEq(dao.getVotes(USER2), balance2, "4: user 2 power"); + assertEq(dao.getVotes(USER3), balance3, "4: user 3 power"); assertEq(xStaking.balanceOf(USER1), balance1, "4: user 1 xStaking balance"); assertEq(xStaking.balanceOf(USER2), balance2, "4: user 2 xStaking balance"); @@ -185,17 +185,17 @@ contract XStakingUpgrade404SonicTest is Test { // ------------------------------- User1 => User3 => User2 => User3 vm.prank(USER1); - stabilityDao.setPowerDelegation(USER3); + dao.setPowerDelegation(USER3); vm.prank(USER3); - stabilityDao.setPowerDelegation(USER2); + dao.setPowerDelegation(USER2); vm.prank(USER2); - stabilityDao.setPowerDelegation(USER3); + dao.setPowerDelegation(USER3); - assertEq(stabilityDao.getVotes(USER1), 0, "5: user 1 power"); - assertEq(stabilityDao.getVotes(USER2), balance3, "5: user 2 power"); - assertEq(stabilityDao.getVotes(USER3), balance1 + balance2, "5: user 3 power"); + assertEq(dao.getVotes(USER1), 0, "5: user 1 power"); + assertEq(dao.getVotes(USER2), balance3, "5: user 2 power"); + assertEq(dao.getVotes(USER3), balance1 + balance2, "5: user 3 power"); assertEq(xStaking.balanceOf(USER1), balance1, "5: user 1 xStaking balance"); assertEq(xStaking.balanceOf(USER2), balance2, "5: user 2 xStaking balance"); @@ -206,9 +206,9 @@ contract XStakingUpgrade404SonicTest is Test { _mintAndDepositToStaking(USER2, balance2); _mintAndDepositToStaking(USER3, balance3); - assertEq(stabilityDao.getVotes(USER1), 0, "6: user 1 power"); - assertEq(stabilityDao.getVotes(USER2), 2 * balance3, "6: user 2 power"); - assertEq(stabilityDao.getVotes(USER3), 2 * (balance1 + balance2), "6: user 3 power"); + assertEq(dao.getVotes(USER1), 0, "6: user 1 power"); + assertEq(dao.getVotes(USER2), 2 * balance3, "6: user 2 power"); + assertEq(dao.getVotes(USER3), 2 * (balance1 + balance2), "6: user 3 power"); assertEq(xStaking.balanceOf(USER1), 2 * balance1, "6: user 1 xStaking balance"); assertEq(xStaking.balanceOf(USER2), 2 * balance2, "6: user 2 xStaking balance"); @@ -221,9 +221,9 @@ contract XStakingUpgrade404SonicTest is Test { vm.prank(USER2); xStaking.withdraw(balance2 * 2); - assertEq(stabilityDao.getVotes(USER1), 0, "7: user 1 power"); - assertEq(stabilityDao.getVotes(USER2), 2 * balance3, "7: user 2 power"); - assertEq(stabilityDao.getVotes(USER3), 0, "7: user 3 power"); + assertEq(dao.getVotes(USER1), 0, "7: user 1 power"); + assertEq(dao.getVotes(USER2), 2 * balance3, "7: user 2 power"); + assertEq(dao.getVotes(USER3), 0, "7: user 3 power"); assertEq(xStaking.balanceOf(USER1), 0, "7: user 1 xStaking balance"); assertEq(xStaking.balanceOf(USER2), 0, "7: user 2 xStaking balance"); @@ -235,26 +235,26 @@ contract XStakingUpgrade404SonicTest is Test { deal(SonicConstantsLib.TOKEN_STBL, user, amount); vm.prank(user); - IERC20(SonicConstantsLib.TOKEN_STBL).approve(address(xStbl), amount); + IERC20(SonicConstantsLib.TOKEN_STBL).approve(address(xToken), amount); vm.prank(user); - xStbl.enter(amount); + xToken.enter(amount); vm.prank(user); - IERC20(address(xStbl)).approve(address(xStaking), amount); + IERC20(address(xToken)).approve(address(xStaking), amount); vm.prank(user); xStaking.deposit(amount); } - function _upgradeAndSetup() internal returns (IStabilityDAO) { - IStabilityDAO stblDaoToken = _createStabilityDAOInstance(); + function _upgradeAndSetup() internal returns (IDAO) { + IDAO dao = _createStabilityDAOInstance(); _upgradePlatform(); vm.prank(multisig); - IPlatform(PLATFORM).setupStabilityDAO(address(stblDaoToken)); + IPlatform(PLATFORM).setupStabilityDAO(address(dao)); - return stblDaoToken; + return dao; } //endregion --------------------------------- Internal logic @@ -273,7 +273,7 @@ contract XStakingUpgrade404SonicTest is Test { proxies[2] = SonicConstantsLib.PLATFORM; implementations[0] = address(new XStaking()); - implementations[1] = address(new XSTBL()); + implementations[1] = address(new XToken()); implementations[2] = address(new Platform()); vm.startPrank(platform.multisig()); @@ -284,8 +284,8 @@ contract XStakingUpgrade404SonicTest is Test { vm.stopPrank(); } - function _createStabilityDAOInstance() internal returns (IStabilityDAO) { - IStabilityDAO.DaoParams memory p = IStabilityDAO.DaoParams({ + function _createStabilityDAOInstance() internal returns (IDAO) { + IDAO.DaoParams memory p = IDAO.DaoParams({ minimalPower: 4000e18, exitPenalty: 50_00, proposalThreshold: 10_000, @@ -294,16 +294,23 @@ contract XStakingUpgrade404SonicTest is Test { }); Proxy proxy = new Proxy(); - proxy.initProxy(address(new StabilityDAO())); - IStabilityDAO token = IStabilityDAO(address(proxy)); - token.initialize(address(PLATFORM), SonicConstantsLib.TOKEN_XSTBL, SonicConstantsLib.XSTBL_XSTAKING, p); + proxy.initProxy(address(new DAO())); + IDAO token = IDAO(address(proxy)); + token.initialize( + address(PLATFORM), + SonicConstantsLib.TOKEN_XSTBL, + SonicConstantsLib.XSTBL_XSTAKING, + p, + "Stability DAO", + "STBL_DAO" + ); return token; } function _updateMinimalPower(uint minimalPower_) internal { IPlatform platform = IPlatform(PLATFORM); - IStabilityDAO daoToken = IStabilityDAO(platform.stabilityDAO()); - IStabilityDAO.DaoParams memory p = daoToken.config(); + IDAO daoToken = IDAO(platform.stabilityDAO()); + IDAO.DaoParams memory p = daoToken.config(); p.minimalPower = minimalPower_; vm.prank(platform.multisig()); diff --git a/test/tokenomics/XStaking.t.sol b/test/tokenomics/XStaking.t.sol index f66846b8..5906447e 100644 --- a/test/tokenomics/XStaking.t.sol +++ b/test/tokenomics/XStaking.t.sol @@ -8,47 +8,49 @@ import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol import {MockSetup} from "../base/MockSetup.sol"; import {Proxy} from "../../src/core/proxy/Proxy.sol"; import {XStaking} from "../../src/tokenomics/XStaking.sol"; -import {XSTBL} from "../../src/tokenomics/XSTBL.sol"; +import {XToken} from "../../src/tokenomics/XToken.sol"; import {RevenueRouter} from "../../src/tokenomics/RevenueRouter.sol"; import {FeeTreasury} from "../../src/tokenomics/FeeTreasury.sol"; -import {IXSTBL} from "../../src/interfaces/IXSTBL.sol"; +import {IXToken} from "../../src/interfaces/IXToken.sol"; import {IXStaking} from "../../src/interfaces/IXStaking.sol"; import {IControllable} from "../../src/interfaces/IControllable.sol"; import {IRevenueRouter} from "../../src/interfaces/IRevenueRouter.sol"; -import {IStabilityDAO} from "../../src/interfaces/IStabilityDAO.sol"; -import {StabilityDAO} from "../../src/tokenomics/StabilityDAO.sol"; +import {IDAO} from "../../src/interfaces/IDAO.sol"; +import {DAO} from "../../src/tokenomics/DAO.sol"; contract XStakingTest is Test, MockSetup { using SafeERC20 for IERC20; - address public stbl; - IXSTBL public xStbl; + address public mainToken; + IXToken public xToken; IXStaking public xStaking; IRevenueRouter public revenueRouter; function setUp() public { - stbl = address(tokenA); + mainToken = address(tokenA); Proxy xStakingProxy = new Proxy(); xStakingProxy.initProxy(address(new XStaking())); - Proxy xSTBLProxy = new Proxy(); - xSTBLProxy.initProxy(address(new XSTBL())); + Proxy xTokenProxy = new Proxy(); + xTokenProxy.initProxy(address(new XToken())); Proxy revenueRouterProxy = new Proxy(); revenueRouterProxy.initProxy(address(new RevenueRouter())); Proxy feeTreasuryProxy = new Proxy(); feeTreasuryProxy.initProxy(address(new FeeTreasury())); FeeTreasury(address(feeTreasuryProxy)).initialize(address(platform), platform.multisig()); - XStaking(address(xStakingProxy)).initialize(address(platform), address(xSTBLProxy)); - XSTBL(address(xSTBLProxy)) - .initialize(address(platform), stbl, address(xStakingProxy), address(revenueRouterProxy)); + XStaking(address(xStakingProxy)).initialize(address(platform), address(xTokenProxy)); + XToken(address(xTokenProxy)) + .initialize( + address(platform), mainToken, address(xStakingProxy), address(revenueRouterProxy), "xStability", "xSTBL" + ); RevenueRouter(address(revenueRouterProxy)) - .initialize(address(platform), address(xSTBLProxy), address(feeTreasuryProxy)); - xStbl = IXSTBL(address(xSTBLProxy)); + .initialize(address(platform), address(xTokenProxy), address(feeTreasuryProxy)); + xToken = IXToken(address(xTokenProxy)); xStaking = IXStaking(address(xStakingProxy)); revenueRouter = IRevenueRouter(address(revenueRouterProxy)); } function test_staking() public { - assertEq(xStaking.xSTBL(), address(xStbl)); + assertEq(xStaking.xToken(), address(xToken)); assertEq(xStaking.lastTimeRewardApplicable(), 0); assertEq(xStaking.totalSupply(), 0); assertEq(xStaking.periodFinish(), 0); @@ -59,32 +61,32 @@ contract XStakingTest is Test, MockSetup { assertEq(xStaking.storedRewardsPerUser(address(1)), 0); assertEq(xStaking.userRewardPerTokenStored(address(1)), 0); - // mint xSTBL + // mint xToken tokenA.mint(100e18); - IERC20(stbl).approve(address(xStbl), 100e18); - xStbl.enter(100e18); + IERC20(mainToken).approve(address(xToken), 100e18); + xToken.enter(100e18); // deposit to staking - IERC20(address(xStbl)).approve(address(xStaking), 100e18); + IERC20(address(xToken)).approve(address(xStaking), 100e18); xStaking.deposit(10e18); assertEq(xStaking.balanceOf(address(this)), 10e18); // make rewards from exit penalties - xStbl.exit(20e18); - assertEq(xStbl.pendingRebase(), 10e18); + xToken.exit(20e18); + assertEq(xToken.pendingRebase(), 10e18); // rebase vm.warp(block.timestamp + 7 days); revenueRouter.updatePeriod(); - assertEq(xStbl.pendingRebase(), 0); - assertGt(xStbl.lastDistributedPeriod(), 0); + assertEq(xToken.pendingRebase(), 0); + assertGt(xToken.lastDistributedPeriod(), 0); vm.warp(block.timestamp + 1 days); // claim rewards - uint balanceWas = IERC20(address(xStbl)).balanceOf(address(this)); + uint balanceWas = IERC20(address(xToken)).balanceOf(address(this)); assertGt(xStaking.earned(address(this)), 0); xStaking.getReward(); - uint balanceChange = IERC20(address(xStbl)).balanceOf(address(this)) - balanceWas; + uint balanceChange = IERC20(address(xToken)).balanceOf(address(this)) - balanceWas; assertGt(balanceChange, 0); xStaking.withdraw(1e18); @@ -120,59 +122,59 @@ contract XStakingTest is Test, MockSetup { // ------------------------------- Bad paths vm.prank(platform.multisig()); - vm.expectRevert(XStaking.StabilityDaoNotInitialized.selector); - xStaking.syncStabilityDAOBalances(users); + vm.expectRevert(XStaking.DaoNotInitialized.selector); + xStaking.syncDAOBalances(users); - // ------------------------------- Mint xSTBL and deposit to staking + // ------------------------------- Mint xToken and deposit to staking for (uint i; i < users.length; ++i) { tokenA.mint(amounts[i]); IERC20(address(tokenA)).safeTransfer(users[i], amounts[i]); vm.prank(users[i]); - IERC20(stbl).approve(address(xStbl), amounts[i]); + IERC20(mainToken).approve(address(xToken), amounts[i]); vm.prank(users[i]); - xStbl.enter(amounts[i]); + xToken.enter(amounts[i]); vm.prank(users[i]); - IERC20(address(xStbl)).approve(address(xStaking), amounts[i]); + IERC20(address(xToken)).approve(address(xStaking), amounts[i]); vm.prank(users[i]); xStaking.deposit(amounts[i]); } // ------------------------------- Set up Stability DAO token - IStabilityDAO stabilityDao = _createStabilityDAOInstance(); + IDAO dao = _createDAOInstance(); vm.prank(platform.multisig()); - platform.setupStabilityDAO(address(stabilityDao)); + platform.setupStabilityDAO(address(dao)); vm.prank(address(123)); vm.expectRevert(IControllable.NotOperator.selector); - xStaking.syncStabilityDAOBalances(users); + xStaking.syncDAOBalances(users); - assertEq(stabilityDao.balanceOf(users[0]), 0, "0: User0 has no dao tokens"); + assertEq(dao.balanceOf(users[0]), 0, "0: User0 has no dao tokens"); assertEq(IERC20(address(xStaking)).balanceOf(users[0]), amounts[0], "0: User0 has xStaking"); - assertEq(stabilityDao.balanceOf(users[1]), 0, "0: User1 has no dao tokens"); + assertEq(dao.balanceOf(users[1]), 0, "0: User1 has no dao tokens"); assertEq(IERC20(address(xStaking)).balanceOf(users[1]), amounts[1], "0: User1 has xStaking"); - assertEq(stabilityDao.balanceOf(users[2]), 0, "0: User2 has no dao tokens"); + assertEq(dao.balanceOf(users[2]), 0, "0: User2 has no dao tokens"); assertEq(IERC20(address(xStaking)).balanceOf(users[2]), amounts[2], "0: User2 has xStaking"); // ------------------------------- sync 1 _updateMinimalPower(4000e18); vm.prank(platform.multisig()); - xStaking.syncStabilityDAOBalances(users); + xStaking.syncDAOBalances(users); - assertEq(stabilityDao.balanceOf(users[0]), 4_001e18, "1: User0"); - assertEq(stabilityDao.balanceOf(users[1]), 0, "1: User1"); - assertEq(stabilityDao.balanceOf(users[2]), 4_000e18, "1: User2"); + assertEq(dao.balanceOf(users[0]), 4_001e18, "1: User0"); + assertEq(dao.balanceOf(users[1]), 0, "1: User1"); + assertEq(dao.balanceOf(users[2]), 4_000e18, "1: User2"); // ------------------------------- sync 2 _updateMinimalPower(3000e18); - assertEq(stabilityDao.balanceOf(users[0]), 4_001e18, "2: User0"); - assertEq(stabilityDao.balanceOf(users[1]), 0, "2: User1 (syncStabilityDAOBalances is not called)"); - assertEq(stabilityDao.balanceOf(users[2]), 4_000e18, "2: User2"); + assertEq(dao.balanceOf(users[0]), 4_001e18, "2: User0"); + assertEq(dao.balanceOf(users[1]), 0, "2: User1 (syncDAOBalances is not called)"); + assertEq(dao.balanceOf(users[2]), 4_000e18, "2: User2"); { address operator = makeAddr("operator"); @@ -181,30 +183,30 @@ contract XStakingTest is Test, MockSetup { platform.addOperator(operator); vm.prank(operator); - xStaking.syncStabilityDAOBalances(users); + xStaking.syncDAOBalances(users); } - assertEq(stabilityDao.balanceOf(users[0]), 4_001e18, "2.1: User0"); - assertEq(stabilityDao.balanceOf(users[1]), 3_999e18, "2.1: User1"); - assertEq(stabilityDao.balanceOf(users[2]), 4_000e18, "2.1: User2"); + assertEq(dao.balanceOf(users[0]), 4_001e18, "2.1: User0"); + assertEq(dao.balanceOf(users[1]), 3_999e18, "2.1: User1"); + assertEq(dao.balanceOf(users[2]), 4_000e18, "2.1: User2"); // ------------------------------- sync 3 _updateMinimalPower(4001e18); - assertEq(stabilityDao.balanceOf(users[0]), 4_001e18, "3: User0"); - assertEq(stabilityDao.balanceOf(users[1]), 3_999e18, "3: User1 (syncStabilityDAOBalances is not called)"); - assertEq(stabilityDao.balanceOf(users[2]), 4_000e18, "3: User2 (syncStabilityDAOBalances is not called)"); + assertEq(dao.balanceOf(users[0]), 4_001e18, "3: User0"); + assertEq(dao.balanceOf(users[1]), 3_999e18, "3: User1 (syncDAOBalances is not called)"); + assertEq(dao.balanceOf(users[2]), 4_000e18, "3: User2 (syncDAOBalances is not called)"); vm.prank(platform.multisig()); - xStaking.syncStabilityDAOBalances(users); + xStaking.syncDAOBalances(users); - assertEq(stabilityDao.balanceOf(users[0]), 4_001e18, "3.1: User0"); - assertEq(stabilityDao.balanceOf(users[1]), 0, "3.1: User1"); - assertEq(stabilityDao.balanceOf(users[2]), 0, "3.1: User2"); + assertEq(dao.balanceOf(users[0]), 4_001e18, "3.1: User0"); + assertEq(dao.balanceOf(users[1]), 0, "3.1: User1"); + assertEq(dao.balanceOf(users[2]), 0, "3.1: User2"); } function testPowerDelegation() public { - IStabilityDAO stabilityDao = _createStabilityDAOInstance(); + IDAO dao = _createDAOInstance(); address[] memory users = new address[](3); users[0] = address(1); @@ -212,88 +214,82 @@ contract XStakingTest is Test, MockSetup { users[2] = address(3); uint72[3] memory amounts = [100e18, 150e18, 300e18]; - // ------------------------------- mint xSTBL and deposit to staking + // ------------------------------- mint xToken and deposit to staking for (uint i; i < users.length; ++i) { tokenA.mint(amounts[i]); IERC20(address(tokenA)).safeTransfer(users[i], amounts[i]); vm.prank(users[i]); - IERC20(stbl).approve(address(xStbl), amounts[i]); + IERC20(mainToken).approve(address(xToken), amounts[i]); vm.prank(users[i]); - xStbl.enter(amounts[i]); + xToken.enter(amounts[i]); } - // ------------------------------- Each user deposits half of their xSTBL to staking + // ------------------------------- Each user deposits half of their xToken to staking for (uint i; i < users.length; ++i) { vm.prank(users[i]); - IERC20(address(xStbl)).approve(address(xStaking), amounts[i]); + IERC20(address(xToken)).approve(address(xStaking), amounts[i]); vm.prank(users[i]); xStaking.deposit(amounts[i] / 2); assertEq(xStaking.balanceOf(users[i]), amounts[i] / 2, "initial balance"); - assertEq(stabilityDao.getVotes(users[i]), 0, "initial power is zero because dao is not initialized"); + assertEq(dao.getVotes(users[i]), 0, "initial power is zero because dao is not initialized"); } // ------------------------------- Initialize Stability DAO and sync users vm.prank(platform.multisig()); - platform.setupStabilityDAO(address(stabilityDao)); + platform.setupStabilityDAO(address(dao)); vm.prank(platform.multisig()); - xStaking.syncStabilityDAOBalances(users); + xStaking.syncDAOBalances(users); for (uint i; i < users.length; ++i) { assertEq(xStaking.balanceOf(users[i]), amounts[i] / 2, "initial balance"); - assertEq(stabilityDao.getVotes(users[i]), amounts[i] / 2, "initial power"); + assertEq(dao.getVotes(users[i]), amounts[i] / 2, "initial power"); } // ------------------------------- 1: 0 => 1 vm.prank(users[0]); - stabilityDao.setPowerDelegation(users[2]); + dao.setPowerDelegation(users[2]); - vm.expectRevert(StabilityDAO.AlreadyDelegated.selector); + vm.expectRevert(IDAO.AlreadyDelegated.selector); vm.prank(users[0]); - stabilityDao.setPowerDelegation(users[2]); + dao.setPowerDelegation(users[2]); vm.prank(users[0]); - stabilityDao.setPowerDelegation(users[0]); + dao.setPowerDelegation(users[0]); vm.prank(users[0]); - stabilityDao.setPowerDelegation(users[1]); + dao.setPowerDelegation(users[1]); - assertEq(stabilityDao.getVotes(users[0]), 0, "1: User 0 delegated his power to user 1"); + assertEq(dao.getVotes(users[0]), 0, "1: User 0 delegated his power to user 1"); assertEq( - stabilityDao.getVotes(users[1]), - amounts[1] / 2 + amounts[0] / 2, - "1: balance user 1 + delegated power of user 0" + dao.getVotes(users[1]), amounts[1] / 2 + amounts[0] / 2, "1: balance user 1 + delegated power of user 0" ); - assertEq(stabilityDao.getVotes(users[2]), amounts[2] / 2, "1: balance user 2"); + assertEq(dao.getVotes(users[2]), amounts[2] / 2, "1: balance user 2"); // ------------------------------- 2: 1 => 2 vm.prank(users[1]); - stabilityDao.setPowerDelegation(users[2]); + dao.setPowerDelegation(users[2]); - assertEq(stabilityDao.getVotes(users[0]), 0, "2: User 0 delegated his power to user 1"); - assertEq(stabilityDao.getVotes(users[1]), amounts[0] / 2, "2: delegated power of user 0"); + assertEq(dao.getVotes(users[0]), 0, "2: User 0 delegated his power to user 1"); + assertEq(dao.getVotes(users[1]), amounts[0] / 2, "2: delegated power of user 0"); assertEq( - stabilityDao.getVotes(users[2]), - amounts[2] / 2 + amounts[1] / 2, - "2: balance user 2 + delegated power of user 1" + dao.getVotes(users[2]), amounts[2] / 2 + amounts[1] / 2, "2: balance user 2 + delegated power of user 1" ); // ------------------------------- A: 2 => 1 vm.prank(users[2]); - stabilityDao.setPowerDelegation(users[1]); + dao.setPowerDelegation(users[1]); - assertEq(stabilityDao.getVotes(users[0]), 0, "A: no power"); - assertEq( - stabilityDao.getVotes(users[1]), amounts[0] / 2 + amounts[2] / 2, "A: delegated power of users 0 and 2" - ); - assertEq(stabilityDao.getVotes(users[2]), amounts[1] / 2, "A: delegated power of user 1"); + assertEq(dao.getVotes(users[0]), 0, "A: no power"); + assertEq(dao.getVotes(users[1]), amounts[0] / 2 + amounts[2] / 2, "A: delegated power of users 0 and 2"); + assertEq(dao.getVotes(users[2]), amounts[1] / 2, "A: delegated power of user 1"); { - (address delegatedTo, address[] memory delegatedFrom) = stabilityDao.delegates(users[1]); + (address delegatedTo, address[] memory delegatedFrom) = dao.delegates(users[1]); assertEq(delegatedTo, users[2], "A: user 1 has delegated his power to user 2"); assertEq(delegatedFrom.length, 2, "A: single user (2) has delegated to user 0"); assertEq(delegatedFrom[0], users[0], "A: user 0 has delegated to user 1"); @@ -302,16 +298,16 @@ contract XStakingTest is Test, MockSetup { // ------------------------------- 3: 2 => 0 vm.prank(users[2]); - stabilityDao.setPowerDelegation(address(0)); + dao.setPowerDelegation(address(0)); vm.prank(users[2]); - stabilityDao.setPowerDelegation(users[0]); + dao.setPowerDelegation(users[0]); - assertEq(stabilityDao.getVotes(users[0]), amounts[2] / 2, "3: delegated power of user 2"); - assertEq(stabilityDao.getVotes(users[1]), amounts[0] / 2, "3: delegated power of user 0"); - assertEq(stabilityDao.getVotes(users[2]), amounts[1] / 2, "3: delegated power of user 1"); + assertEq(dao.getVotes(users[0]), amounts[2] / 2, "3: delegated power of user 2"); + assertEq(dao.getVotes(users[1]), amounts[0] / 2, "3: delegated power of user 0"); + assertEq(dao.getVotes(users[2]), amounts[1] / 2, "3: delegated power of user 1"); - // ------------------------------- 4: Each user deposits second half of their xSTBL to staking + // ------------------------------- 4: Each user deposits second half of their xToken to staking for (uint i; i < users.length; ++i) { vm.prank(users[i]); xStaking.deposit(amounts[i] / 2); @@ -319,61 +315,61 @@ contract XStakingTest is Test, MockSetup { assertEq(xStaking.balanceOf(users[i]), amounts[i], "full balance"); } - assertEq(stabilityDao.getVotes(users[0]), amounts[2], "4: delegated power of user 2"); - assertEq(stabilityDao.getVotes(users[1]), amounts[0], "4: delegated power of user 0"); - assertEq(stabilityDao.getVotes(users[2]), amounts[1], "4: delegated power of user 1"); + assertEq(dao.getVotes(users[0]), amounts[2], "4: delegated power of user 2"); + assertEq(dao.getVotes(users[1]), amounts[0], "4: delegated power of user 0"); + assertEq(dao.getVotes(users[2]), amounts[1], "4: delegated power of user 1"); // ------------------------------- 5: User 1 withdraws half of his stake vm.prank(users[1]); xStaking.withdraw(amounts[1] / 2); - assertEq(stabilityDao.getVotes(users[0]), amounts[2], "5: delegated power of user 2"); - assertEq(stabilityDao.getVotes(users[1]), amounts[0], "5: delegated power of user 0"); - assertEq(stabilityDao.getVotes(users[2]), amounts[1] / 2, "5: delegated power of user 1"); + assertEq(dao.getVotes(users[0]), amounts[2], "5: delegated power of user 2"); + assertEq(dao.getVotes(users[1]), amounts[0], "5: delegated power of user 0"); + assertEq(dao.getVotes(users[2]), amounts[1] / 2, "5: delegated power of user 1"); // ------------------------------- 6: User 1 removes delegation vm.prank(users[1]); - stabilityDao.setPowerDelegation(users[1]); + dao.setPowerDelegation(users[1]); - assertEq(stabilityDao.getVotes(users[0]), amounts[2], "6: delegated power of user 2"); - assertEq(stabilityDao.getVotes(users[1]), amounts[1] / 2 + amounts[0], "6: delegated power of user 0"); - assertEq(stabilityDao.getVotes(users[2]), 0, "6: all power was delegated to user 0"); + assertEq(dao.getVotes(users[0]), amounts[2], "6: delegated power of user 2"); + assertEq(dao.getVotes(users[1]), amounts[1] / 2 + amounts[0], "6: delegated power of user 0"); + assertEq(dao.getVotes(users[2]), 0, "6: all power was delegated to user 0"); { - (address delegatedTo, address[] memory delegatedFrom) = stabilityDao.delegates(users[0]); + (address delegatedTo, address[] memory delegatedFrom) = dao.delegates(users[0]); assertEq(delegatedTo, users[1], "6: user 0 has delegated his power to user 1"); assertEq(delegatedFrom.length, 1, "6: single user (2) has delegated to user 0"); assertEq(delegatedFrom[0], users[2], "6: user 2 has delegated to user 0"); } { - (address delegatedTo, address[] memory delegatedFrom) = stabilityDao.delegates(users[1]); + (address delegatedTo, address[] memory delegatedFrom) = dao.delegates(users[1]); assertEq(delegatedTo, address(0), "6: user 1 has not delegated power"); assertEq(delegatedFrom.length, 1, "6: single user (0) has delegated to user 1"); assertEq(delegatedFrom[0], users[0], "6: user 0 has delegated to user 1"); } { - (address delegatedTo, address[] memory delegatedFrom) = stabilityDao.delegates(users[2]); + (address delegatedTo, address[] memory delegatedFrom) = dao.delegates(users[2]); assertEq(delegatedTo, users[0], "6: user 2 has delegated his power to user 0"); assertEq(delegatedFrom.length, 0, "6: no one has delegated to user 2"); } // ------------------------------- 7: Users 0 and 2 remove delegations vm.prank(users[0]); - stabilityDao.setPowerDelegation(users[0]); // remove using delegation to oneself + dao.setPowerDelegation(users[0]); // remove using delegation to oneself vm.prank(users[2]); - stabilityDao.setPowerDelegation(address(0)); // remove using zero address + dao.setPowerDelegation(address(0)); // remove using zero address - assertEq(stabilityDao.getVotes(users[0]), amounts[0], "7: user 0 has not delegated power"); - assertEq(stabilityDao.getVotes(users[1]), amounts[1] / 2, "7: user 1 has not delegated power"); - assertEq(stabilityDao.getVotes(users[2]), amounts[2], "7: user 2 has not delegated power"); + assertEq(dao.getVotes(users[0]), amounts[0], "7: user 0 has not delegated power"); + assertEq(dao.getVotes(users[1]), amounts[1] / 2, "7: user 1 has not delegated power"); + assertEq(dao.getVotes(users[2]), amounts[2], "7: user 2 has not delegated power"); } //region --------------------------------- Utils - function _createStabilityDAOInstance() internal returns (IStabilityDAO) { - IStabilityDAO.DaoParams memory p = IStabilityDAO.DaoParams({ + function _createDAOInstance() internal returns (IDAO) { + IDAO.DaoParams memory p = IDAO.DaoParams({ minimalPower: 50e18, exitPenalty: 50_00, proposalThreshold: 10_000, @@ -382,15 +378,15 @@ contract XStakingTest is Test, MockSetup { }); Proxy proxy = new Proxy(); - proxy.initProxy(address(new StabilityDAO())); - IStabilityDAO token = IStabilityDAO(address(proxy)); - token.initialize(address(platform), address(xStbl), address(xStaking), p); + proxy.initProxy(address(new DAO())); + IDAO token = IDAO(address(proxy)); + token.initialize(address(platform), address(xToken), address(xStaking), p, "Stability DAO", "STBL_DAO"); return token; } function _updateMinimalPower(uint minimalPower_) internal { - IStabilityDAO daoToken = IStabilityDAO(platform.stabilityDAO()); - IStabilityDAO.DaoParams memory p = daoToken.config(); + IDAO daoToken = IDAO(platform.stabilityDAO()); + IDAO.DaoParams memory p = daoToken.config(); p.minimalPower = minimalPower_; vm.prank(platform.multisig()); diff --git a/test/tokenomics/XSTBL.Upgrade.406.t.sol b/test/tokenomics/XToken.Upgrade.406.t.sol similarity index 68% rename from test/tokenomics/XSTBL.Upgrade.406.t.sol rename to test/tokenomics/XToken.Upgrade.406.t.sol index 97e67f2b..a4fbc038 100644 --- a/test/tokenomics/XSTBL.Upgrade.406.t.sol +++ b/test/tokenomics/XToken.Upgrade.406.t.sol @@ -7,14 +7,14 @@ import {SonicConstantsLib} from "../../chains/sonic/SonicConstantsLib.sol"; import {IPlatform} from "../../src/interfaces/IPlatform.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {IRevenueRouter} from "../../src/tokenomics/RevenueRouter.sol"; -import {IXSTBL} from "../../src/interfaces/IXSTBL.sol"; -import {IStabilityDAO} from "../../src/interfaces/IStabilityDAO.sol"; -import {XSTBL} from "../../src/tokenomics/XSTBL.sol"; +import {IXToken} from "../../src/interfaces/IXToken.sol"; +import {IDAO} from "../../src/interfaces/IDAO.sol"; +import {XToken} from "../../src/tokenomics/XToken.sol"; import {Platform} from "../../src/core/Platform.sol"; import {Proxy} from "../../src/core/proxy/Proxy.sol"; -import {StabilityDAO} from "../../src/tokenomics/StabilityDAO.sol"; +import {DAO} from "../../src/tokenomics/DAO.sol"; -contract XstblUpgrade406SonicTest is Test { +contract XTokenUpgrade406SonicTest is Test { uint public constant FORK_BLOCK = 50689527; // Oct-15-2025 05:17:06 AM +UTC address public constant PLATFORM = SonicConstantsLib.PLATFORM; IRevenueRouter internal revenueRouter; @@ -25,35 +25,35 @@ contract XstblUpgrade406SonicTest is Test { } function testUpgradeXSTBLVesting() public { - IXSTBL xstbl = IXSTBL(SonicConstantsLib.TOKEN_XSTBL); + IXToken xToken = IXToken(SonicConstantsLib.TOKEN_XSTBL); _upgradePlatform(); - IStabilityDAO daoToken = _setupStblDao(); + IDAO daoToken = _setupStblDao(); uint baseAmount = 100e18; // -------------- get STBL on balance deal(SonicConstantsLib.TOKEN_STBL, address(this), baseAmount); - // -------------- enter to xSTBL - IERC20(SonicConstantsLib.TOKEN_STBL).approve(address(xstbl), type(uint).max); - xstbl.enter(baseAmount); - uint xstblBalance = IERC20(address(xstbl)).balanceOf(address(this)); - assertEq(xstblBalance, baseAmount, "xstbl balance after enter"); + // -------------- enter to xToken + IERC20(SonicConstantsLib.TOKEN_STBL).approve(address(xToken), type(uint).max); + xToken.enter(baseAmount); + uint xTokenBalance = IERC20(address(xToken)).balanceOf(address(this)); + assertEq(xTokenBalance, baseAmount, "xToken balance after enter"); // -------------- create vest - xstbl.createVest(baseAmount); - assertEq(xstbl.usersTotalVests(address(this)), 1, "now user has a vest"); + xToken.createVest(baseAmount); + assertEq(xToken.usersTotalVests(address(this)), 1, "now user has a vest"); // -------------- wait min period (14 days) to be able to exit vest w/o cancellation skip(14 days); // -------------- try to exit vest with penalty 50% and check results - (uint exitedAmount50, uint pendingRebaseDelta50) = _tryToExitVest(xstbl, address(this), 0); + (uint exitedAmount50, uint pendingRebaseDelta50) = _tryToExitVest(xToken, address(this), 0); // -------------- change penalty to 80% { - IStabilityDAO.DaoParams memory p = daoToken.config(); + IDAO.DaoParams memory p = daoToken.config(); p.exitPenalty = 80_00; vm.prank(SonicConstantsLib.MULTISIG); @@ -61,7 +61,7 @@ contract XstblUpgrade406SonicTest is Test { } // -------------- try to exit vest with penalty 80% and check results - (uint exitedAmount20, uint pendingRebaseDelta80) = _tryToExitVest(xstbl, address(this), 0); + (uint exitedAmount20, uint pendingRebaseDelta80) = _tryToExitVest(xToken, address(this), 0); // -------------- check results (14 days of 180 were passed) assertEq(exitedAmount50, baseAmount * (100 - 50) / 100 + baseAmount * 50 / 100 * 14 / 180, "exitedAmount50"); @@ -71,27 +71,27 @@ contract XstblUpgrade406SonicTest is Test { assertEq(exitedAmount20 + pendingRebaseDelta80, baseAmount, "80: total 100%"); } - function testUpgradeXSTBLExit() public { - IXSTBL xstbl = IXSTBL(SonicConstantsLib.TOKEN_XSTBL); + function testUpgradeXTokenExit() public { + IXToken xToken = IXToken(SonicConstantsLib.TOKEN_XSTBL); _upgradePlatform(); - IStabilityDAO daoToken = _setupStblDao(); + IDAO daoToken = _setupStblDao(); uint baseAmount = 100e18; // -------------- get STBL on balance deal(SonicConstantsLib.TOKEN_STBL, address(this), baseAmount); - // -------------- enter to xSTBL - IERC20(SonicConstantsLib.TOKEN_STBL).approve(address(xstbl), type(uint).max); - xstbl.enter(baseAmount); - uint xstblBalance = IERC20(address(xstbl)).balanceOf(address(this)); - assertEq(xstblBalance, baseAmount, "xstbl balance after enter"); + // -------------- enter to xToken + IERC20(SonicConstantsLib.TOKEN_STBL).approve(address(xToken), type(uint).max); + xToken.enter(baseAmount); + uint xTokenBalance = IERC20(address(xToken)).balanceOf(address(this)); + assertEq(xTokenBalance, baseAmount, "xToken balance after enter"); // -------------- try to exit vest with penalty 50% and check results - (uint exitedAmount50, uint pendingRebaseDelta50) = _tryToExit(xstbl, address(this), baseAmount); + (uint exitedAmount50, uint pendingRebaseDelta50) = _tryToExit(xToken, address(this), baseAmount); // -------------- change penalty to 80% { - IStabilityDAO.DaoParams memory p = daoToken.config(); + IDAO.DaoParams memory p = daoToken.config(); p.exitPenalty = 80_00; vm.prank(SonicConstantsLib.MULTISIG); @@ -99,7 +99,7 @@ contract XstblUpgrade406SonicTest is Test { } // -------------- try to exit vest with penalty 80% and check results - (uint exitedAmount20, uint pendingRebaseDelta80) = _tryToExit(xstbl, address(this), baseAmount); + (uint exitedAmount20, uint pendingRebaseDelta80) = _tryToExit(xToken, address(this), baseAmount); // -------------- check results (14 days of 180 were passed) assertEq(exitedAmount50, baseAmount * 50 / 100, "exitedAmount 50%"); @@ -112,8 +112,8 @@ contract XstblUpgrade406SonicTest is Test { assertEq(exitedAmount20 + pendingRebaseDelta80, baseAmount, "total 100%"); } - function testUpgradeXSTBLExitNoStabilityDao() public { - IXSTBL xstbl = IXSTBL(SonicConstantsLib.TOKEN_XSTBL); + function testUpgradeXTokenExitNoStabilityDao() public { + IXToken xToken = IXToken(SonicConstantsLib.TOKEN_XSTBL); _upgradePlatform(); // Stability DAO is not initialized @@ -122,14 +122,14 @@ contract XstblUpgrade406SonicTest is Test { // -------------- get STBL on balance deal(SonicConstantsLib.TOKEN_STBL, address(this), baseAmount); - // -------------- enter to xSTBL - IERC20(SonicConstantsLib.TOKEN_STBL).approve(address(xstbl), type(uint).max); - xstbl.enter(baseAmount); - uint xstblBalance = IERC20(address(xstbl)).balanceOf(address(this)); - assertEq(xstblBalance, baseAmount, "xstbl balance after enter"); + // -------------- enter to xToken + IERC20(SonicConstantsLib.TOKEN_STBL).approve(address(xToken), type(uint).max); + xToken.enter(baseAmount); + uint xTokenBalance = IERC20(address(xToken)).balanceOf(address(this)); + assertEq(xTokenBalance, baseAmount, "xToken balance after enter"); // -------------- try to exit vest with penalty 50% and check results - (uint exitedAmount50, uint pendingRebaseDelta50) = _tryToExit(xstbl, address(this), baseAmount); + (uint exitedAmount50, uint pendingRebaseDelta50) = _tryToExit(xToken, address(this), baseAmount); // -------------- check results (14 days of 180 were passed) assertEq(exitedAmount50, baseAmount * 50 / 100, "exitedAmount 50%"); @@ -139,17 +139,17 @@ contract XstblUpgrade406SonicTest is Test { //region -------------------------------- Internal logic function _tryToExitVest( - IXSTBL xstbl, + IXToken xToken_, address user, uint vestId ) internal returns (uint exitedAmount, uint pendingRebaseDelta) { uint snapshot = vm.snapshotState(); - uint pendingRebaseBefore = IXSTBL(SonicConstantsLib.TOKEN_XSTBL).pendingRebase(); + uint pendingRebaseBefore = IXToken(SonicConstantsLib.TOKEN_XSTBL).pendingRebase(); uint balanceBefore = IERC20(SonicConstantsLib.TOKEN_STBL).balanceOf(user); - xstbl.exitVest(vestId); + xToken_.exitVest(vestId); uint balanceAfter = IERC20(SonicConstantsLib.TOKEN_STBL).balanceOf(user); - uint pendingRebaseAfter = IXSTBL(SonicConstantsLib.TOKEN_XSTBL).pendingRebase(); + uint pendingRebaseAfter = IXToken(SonicConstantsLib.TOKEN_XSTBL).pendingRebase(); exitedAmount = balanceAfter - balanceBefore; pendingRebaseDelta = pendingRebaseAfter - pendingRebaseBefore; @@ -158,17 +158,17 @@ contract XstblUpgrade406SonicTest is Test { } function _tryToExit( - IXSTBL xstbl, + IXToken xToken_, address user, uint amount ) internal returns (uint exitedAmount, uint pendingRebaseDelta) { uint snapshot = vm.snapshotState(); - uint pendingRebaseBefore = IXSTBL(SonicConstantsLib.TOKEN_XSTBL).pendingRebase(); + uint pendingRebaseBefore = IXToken(SonicConstantsLib.TOKEN_XSTBL).pendingRebase(); uint balanceBefore = IERC20(SonicConstantsLib.TOKEN_STBL).balanceOf(user); - xstbl.exit(amount); + xToken_.exit(amount); uint balanceAfter = IERC20(SonicConstantsLib.TOKEN_STBL).balanceOf(user); - uint pendingRebaseAfter = IXSTBL(SonicConstantsLib.TOKEN_XSTBL).pendingRebase(); + uint pendingRebaseAfter = IXToken(SonicConstantsLib.TOKEN_XSTBL).pendingRebase(); exitedAmount = balanceAfter - balanceBefore; pendingRebaseDelta = pendingRebaseAfter - pendingRebaseBefore; @@ -190,7 +190,7 @@ contract XstblUpgrade406SonicTest is Test { proxies[0] = SonicConstantsLib.TOKEN_XSTBL; proxies[1] = SonicConstantsLib.PLATFORM; - implementations[0] = address(new XSTBL()); + implementations[0] = address(new XToken()); implementations[1] = address(new Platform()); vm.startPrank(SonicConstantsLib.MULTISIG); @@ -204,16 +204,16 @@ contract XstblUpgrade406SonicTest is Test { vm.stopPrank(); } - function _setupStblDao() internal returns (IStabilityDAO) { - IStabilityDAO dest = _createStabilityDAOInstance(); + function _setupStblDao() internal returns (IDAO) { + IDAO dest = _createStabilityDAOInstance(); vm.prank(SonicConstantsLib.MULTISIG); IPlatform(SonicConstantsLib.PLATFORM).setupStabilityDAO(address(dest)); return dest; } - function _createStabilityDAOInstance() internal returns (IStabilityDAO) { - IStabilityDAO.DaoParams memory p = IStabilityDAO.DaoParams({ + function _createStabilityDAOInstance() internal returns (IDAO) { + IDAO.DaoParams memory p = IDAO.DaoParams({ minimalPower: 4000e18, exitPenalty: 0, // default 50% quorum: 15_000, @@ -222,9 +222,16 @@ contract XstblUpgrade406SonicTest is Test { }); Proxy proxy = new Proxy(); - proxy.initProxy(address(new StabilityDAO())); - IStabilityDAO token = IStabilityDAO(address(proxy)); - token.initialize(SonicConstantsLib.PLATFORM, SonicConstantsLib.TOKEN_STBL, SonicConstantsLib.XSTBL_XSTAKING, p); + proxy.initProxy(address(new DAO())); + IDAO token = IDAO(address(proxy)); + token.initialize( + SonicConstantsLib.PLATFORM, + SonicConstantsLib.TOKEN_STBL, + SonicConstantsLib.XSTBL_XSTAKING, + p, + "Stability DAO", + "STBL_DAO" + ); return token; } diff --git a/test/tokenomics/XToken.Upgrade.424.t.sol b/test/tokenomics/XToken.Upgrade.424.t.sol new file mode 100644 index 00000000..2339044e --- /dev/null +++ b/test/tokenomics/XToken.Upgrade.424.t.sol @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +// import {console} from "forge-std/console.sol"; +import {Test} from "forge-std/Test.sol"; +import {SonicConstantsLib} from "../../chains/sonic/SonicConstantsLib.sol"; +import {IPlatform} from "../../src/interfaces/IPlatform.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IXToken} from "../../src/interfaces/IXToken.sol"; +import {IXStaking} from "../../src/interfaces/IXStaking.sol"; +import {IDAO} from "../../src/interfaces/IDAO.sol"; +import {XToken} from "../../src/tokenomics/XToken.sol"; +import {DAO} from "../../src/tokenomics/DAO.sol"; +import {XStaking} from "../../src/tokenomics/XStaking.sol"; + +/// @notice Test upgrade of xToken and DAO +contract XTokenUpgrade424SonicTest is Test { + uint public constant FORK_BLOCK = 57497805; // Dec-09-2025 05:04:40 AM +UTC + address public constant PLATFORM = SonicConstantsLib.PLATFORM; + + constructor() { + vm.selectFork(vm.createFork(vm.envString("SONIC_RPC_URL"), FORK_BLOCK)); + } + + function testUpgradeNotDataChanged() public { + IXToken xToken = IXToken(SonicConstantsLib.TOKEN_XSTBL); + IDAO daoToken = IDAO(IPlatform(PLATFORM).stabilityDAO()); + IXStaking xStaking = IXStaking(SonicConstantsLib.XSTBL_XSTAKING); + + uint baseAmount = 100e18; + + // -------------- get STBL on balance + deal(SonicConstantsLib.TOKEN_STBL, address(this), baseAmount); + + // -------------- enter to xToken + IERC20(SonicConstantsLib.TOKEN_STBL).approve(address(xToken), type(uint).max); + xToken.enter(baseAmount); + + uint xTokenBalance = IERC20(address(xToken)).balanceOf(address(this)); + assertEq(xTokenBalance, baseAmount, "xToken balance after enter"); + + // -------------- get config of DAO + IDAO.DaoParams memory daoParamsBefore = daoToken.config(); + + _upgradePlatform(); + + vm.prank(SonicConstantsLib.MULTISIG); + xToken.setName("xStabilityV2"); + + vm.prank(SonicConstantsLib.MULTISIG); + xToken.setSymbol("xSTBLv2"); + + vm.prank(SonicConstantsLib.MULTISIG); + daoToken.setName("StabilityDAOv2"); + + vm.prank(SonicConstantsLib.MULTISIG); + daoToken.setSymbol("STBLDAOv2"); + + (uint exitedAmount,) = _tryToExit(xToken, address(this), baseAmount); + assertEq(exitedAmount, baseAmount * (1e4 - daoParamsBefore.exitPenalty) / 1e4, "exited amount after upgrade"); + + assertEq(xToken.xStaking(), SonicConstantsLib.XSTBL_XSTAKING, "xStaking address mismatch"); + assertEq(xToken.token(), SonicConstantsLib.TOKEN_STBL, "main token address mismatch"); + + assertEq(xStaking.xToken(), SonicConstantsLib.TOKEN_XSTBL, "xToken address mismatch"); + + // -------------- check config of DAO after upgrade + IDAO.DaoParams memory daoParamsAfter = daoToken.config(); + + assertEq(daoParamsBefore.exitPenalty, daoParamsAfter.exitPenalty, "exitPenalty"); + assertEq(daoParamsBefore.minimalPower, daoParamsAfter.minimalPower, "minimalPower"); + assertEq(daoParamsBefore.proposalThreshold, daoParamsAfter.proposalThreshold, "proposalThreshold"); + assertEq(daoParamsBefore.quorum, daoParamsAfter.quorum, "quorum"); + assertEq(daoParamsBefore.powerAllocationDelay, daoParamsAfter.powerAllocationDelay, "powerAllocationDelay"); + } + + //region -------------------------------- Internal logic + function _tryToExit( + IXToken xToken_, + address user, + uint amount + ) internal returns (uint exitedAmount, uint pendingRebaseDelta) { + uint snapshot = vm.snapshotState(); + + uint pendingRebaseBefore = IXToken(SonicConstantsLib.TOKEN_XSTBL).pendingRebase(); + uint balanceBefore = IERC20(SonicConstantsLib.TOKEN_STBL).balanceOf(user); + xToken_.exit(amount); + uint balanceAfter = IERC20(SonicConstantsLib.TOKEN_STBL).balanceOf(user); + uint pendingRebaseAfter = IXToken(SonicConstantsLib.TOKEN_XSTBL).pendingRebase(); + + exitedAmount = balanceAfter - balanceBefore; + pendingRebaseDelta = pendingRebaseAfter - pendingRebaseBefore; + + vm.revertToState(snapshot); + } + + //endregion -------------------------------- Internal logic + + //region -------------------------------- Helpers + function _upgradePlatform() internal { + rewind(1 days); + + IPlatform platform = IPlatform(PLATFORM); + + address[] memory proxies = new address[](3); + address[] memory implementations = new address[](3); + + proxies[0] = SonicConstantsLib.TOKEN_XSTBL; + proxies[1] = platform.stabilityDAO(); + proxies[2] = SonicConstantsLib.XSTBL_XSTAKING; + + implementations[0] = address(new XToken()); + implementations[1] = address(new DAO()); + implementations[2] = address(new XStaking()); + + // vm.startPrank(SonicConstantsLib.MULTISIG); + // platform.cancelUpgrade(); + + vm.startPrank(SonicConstantsLib.MULTISIG); + platform.announcePlatformUpgrade("2025.12.00-alpha", proxies, implementations); + + skip(1 days); + platform.upgrade(); + vm.stopPrank(); + } + //endregion -------------------------------- Helpers +} diff --git a/test/tokenomics/XToken.t.sol b/test/tokenomics/XToken.t.sol new file mode 100644 index 00000000..e8f39ae5 --- /dev/null +++ b/test/tokenomics/XToken.t.sol @@ -0,0 +1,337 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import {Test} from "forge-std/Test.sol"; +import {MockSetup} from "../base/MockSetup.sol"; +import {Proxy} from "../../src/core/proxy/Proxy.sol"; +import {IXToken} from "../../src/interfaces/IXToken.sol"; +import {IXStaking} from "../../src/interfaces/IXStaking.sol"; +import {IDAO} from "../../src/interfaces/IDAO.sol"; +import {IControllable} from "../../src/interfaces/IControllable.sol"; +import {XStaking} from "../../src/tokenomics/XStaking.sol"; +import {XToken} from "../../src/tokenomics/XToken.sol"; +import {RevenueRouter} from "../../src/tokenomics/RevenueRouter.sol"; +import {FeeTreasury} from "../../src/tokenomics/FeeTreasury.sol"; +import {DAO} from "../../src/tokenomics/DAO.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +// import {console} from "forge-std/console.sol"; + +contract XTokenTest is Test, MockSetup { + using SafeERC20 for IERC20; + + address public stbl; + IXToken public xToken; + IXStaking public xStaking; + + //region --------------------- Tests + function setUp() public { + stbl = address(tokenA); + Proxy xStakingProxy = new Proxy(); + xStakingProxy.initProxy(address(new XStaking())); + Proxy xTokenProxy = new Proxy(); + xTokenProxy.initProxy(address(new XToken())); + Proxy revenueRouterProxy = new Proxy(); + revenueRouterProxy.initProxy(address(new RevenueRouter())); + Proxy feeTreasuryProxy = new Proxy(); + feeTreasuryProxy.initProxy(address(new FeeTreasury())); + FeeTreasury(address(feeTreasuryProxy)).initialize(address(platform), platform.multisig()); + XStaking(address(xStakingProxy)).initialize(address(platform), address(xTokenProxy)); + XToken(address(xTokenProxy)) + .initialize( + address(platform), stbl, address(xStakingProxy), address(revenueRouterProxy), "xStability", "xSTBL" + ); + RevenueRouter(address(revenueRouterProxy)) + .initialize(address(platform), address(xTokenProxy), address(feeTreasuryProxy)); + xToken = IXToken(address(xTokenProxy)); + xStaking = IXStaking(address(xStakingProxy)); + //console.logBytes32(keccak256(abi.encode(uint256(keccak256("erc7201:stability.XSTBL")) - 1)) & ~bytes32(uint256(0xff))); + } + + function test_transfer() public { + tokenA.mint(100e18); + IERC20(stbl).approve(address(xToken), 100e18); + xToken.enter(100e18); + + vm.expectRevert(abi.encodeWithSelector(IXToken.NOT_WHITELISTED.selector, address(this), address(1))); + /// forge-lint: disable-next-line + IERC20(address(xToken)).transfer(address(1), 1e18); + + address[] memory exemptee = new address[](1); + exemptee[0] = address(this); + bool[] memory exempt = new bool[](2); + vm.expectRevert(abi.encodeWithSelector(IControllable.IncorrectArrayLength.selector)); + xToken.setExemptionFrom(exemptee, exempt); + vm.expectRevert(abi.encodeWithSelector(IControllable.IncorrectArrayLength.selector)); + xToken.setExemptionTo(exemptee, exempt); + vm.prank(address(101)); + vm.expectRevert(abi.encodeWithSelector(IControllable.NotGovernanceAndNotMultisig.selector)); + xToken.setExemptionFrom(exemptee, exempt); + vm.prank(address(101)); + vm.expectRevert(abi.encodeWithSelector(IControllable.NotGovernanceAndNotMultisig.selector)); + xToken.setExemptionTo(exemptee, exempt); + exempt = new bool[](1); + exempt[0] = true; + xToken.setExemptionFrom(exemptee, exempt); + + /// forge-lint: disable-next-line + IERC20(address(xToken)).transfer(address(1), 1e18); + + exempt[0] = false; + xToken.setExemptionFrom(exemptee, exempt); + vm.expectRevert(abi.encodeWithSelector(IXToken.NOT_WHITELISTED.selector, address(this), address(1))); + /// forge-lint: disable-next-line + IERC20(address(xToken)).transfer(address(1), 1e18); + + exemptee[0] = address(1); + exempt[0] = true; + xToken.setExemptionTo(exemptee, exempt); + /// forge-lint: disable-next-line + IERC20(address(xToken)).transfer(address(1), 1e18); + + vm.prank(address(1)); + vm.expectRevert(abi.encodeWithSelector(IXToken.NOT_WHITELISTED.selector, address(1), address(2))); + /// forge-lint: disable-next-line + IERC20(address(xToken)).transfer(address(2), 1e18); + } + + function test_enter_exit() public { + tokenA.mint(100e18); + IERC20(stbl).approve(address(xToken), 100e18); + + // enter + xToken.enter(100e18); + assertEq(IERC20(address(xToken)).balanceOf(address(this)), 100e18); + assertEq(IERC20(stbl).balanceOf(address(xToken)), 100e18); + + // instant exit + xToken.exit(50e18); + assertEq(IERC20(address(xToken)).balanceOf(address(this)), 50e18); + assertEq(IERC20(stbl).balanceOf(address(this)), 25e18); + assertEq(IERC20(stbl).balanceOf(address(xToken)), 75e18); + + // create vest + uint time = block.timestamp; + xToken.createVest(30e18); + (uint amount, uint start, uint maxEnd) = xToken.vestInfo(address(this), 0); + assertEq(amount, 30e18); + assertEq(start, time); + assertEq(maxEnd, time + xToken.MAX_VEST()); + assertEq(xToken.usersTotalVests(address(this)), 1); + assertEq(IERC20(address(xToken)).balanceOf(address(this)), 20e18); + + // cancel vesting + vm.warp(time + 13 days); + xToken.exitVest(0); + (amount,,) = xToken.vestInfo(address(this), 0); + assertEq(amount, 0); + assertEq(IERC20(address(xToken)).balanceOf(address(this)), 50e18); + assertEq(xToken.pendingRebase(), 25e18); + + // exit vesting in progress + time = block.timestamp; + xToken.createVest(30e18); + assertEq(xToken.usersTotalVests(address(this)), 2); + vm.warp(time + 179 days); + xToken.exitVest(1); + (amount,,) = xToken.vestInfo(address(this), 1); + assertEq(amount, 0); + assertGt(IERC20(stbl).balanceOf(address(this)), 25e18 + 29e18); + assertLt(IERC20(stbl).balanceOf(address(this)), 25e18 + 30e18); + + // exit completed vesting + time = block.timestamp; + xToken.createVest(20e18); + vm.warp(time + 200 days); + uint balanceWas = IERC20(stbl).balanceOf(address(this)); + xToken.exitVest(2); + assertEq(IERC20(stbl).balanceOf(address(this)), balanceWas + 20e18); + } + + function test_reverts() public { + vm.expectRevert(); + xToken.rebase(); + + vm.expectRevert(); + xToken.enter(0); + + vm.expectRevert(); + xToken.exit(0); + + vm.expectRevert(); + xToken.createVest(0); + + vm.expectRevert(); + xToken.exitVest(10); + } + + function testSlashingPenalty() public { + // --------------------- StabilityDAO is not initialized + assertEq(xToken.SLASHING_PENALTY(), 50_00, "50% by default"); + + // --------------------- Set up StabilityDAO + IDAO daoToken = _createDAOInstance(); + platform.setupStabilityDAO(address(daoToken)); + + _setSlashingPenalty(daoToken, 80_00); + assertEq(xToken.SLASHING_PENALTY(), 80_00, "80%"); + + _setSlashingPenalty(daoToken, 30_00); + assertEq(xToken.SLASHING_PENALTY(), 30_00, "30%"); + + _setSlashingPenalty(daoToken, 0); + assertEq(xToken.SLASHING_PENALTY(), 50_00, "DEFAULT_SLASHING_PENALTY"); + } + + function testSetBridge() public { + address multisig = platform.multisig(); + + assertEq(xToken.isBridge(address(1)), false, "not bridge by default"); + + vm.expectRevert(IControllable.NotGovernanceAndNotMultisig.selector); + vm.prank(address(2)); + xToken.setBridge(address(1), true); + + vm.prank(multisig); + xToken.setBridge(address(1), true); + + assertEq(xToken.isBridge(address(1)), true, "expected bridge"); + + vm.prank(multisig); + xToken.setBridge(address(1), false); + + assertEq(xToken.isBridge(address(1)), false, "bridge is cleared"); + } + + function testBridgeActions() public { + address multisig = platform.multisig(); + address bridge = makeAddr("bridge"); + address user = makeAddr("user"); + + vm.prank(multisig); + xToken.setBridge(bridge, true); + + // ---------------------- prepare xToken for the user + tokenA.mint(100e18); + IERC20(address(tokenA)).safeTransfer(user, 100e18); + + vm.prank(user); + IERC20(stbl).approve(address(xToken), 100e18); + + vm.prank(user); + xToken.enter(100e18); + + // ---------------------- send xToken to the bridge + { + vm.expectRevert(IControllable.IncorrectZeroArgument.selector); + vm.prank(bridge); + xToken.sendToBridge(address(0), 1e18); + + vm.expectRevert(IControllable.IncorrectZeroArgument.selector); + vm.prank(bridge); + xToken.sendToBridge(user, 0); + + vm.expectRevert(IControllable.IncorrectMsgSender.selector); + vm.prank(user); + xToken.sendToBridge(bridge, 1e18); + + assertEq(IERC20(address(xToken)).balanceOf(user), 100e18, "user xToken balance before sendToBridge"); + assertEq(IERC20(address(tokenA)).balanceOf(user), 0, "user main-token balance before sendToBridge"); + + vm.prank(bridge); + xToken.sendToBridge(user, 40e18); + } + + assertEq(IERC20(address(xToken)).balanceOf(user), 60e18, "user xToken balance after sendToBridge"); + assertEq( + IERC20(address(tokenA)).balanceOf(address(xToken)), 60e18, "locked main-token balance after sendToBridge" + ); + assertEq(IERC20(address(tokenA)).balanceOf(user), 0, "user main-token balance after sendToBridge"); + assertEq(IERC20(address(tokenA)).balanceOf(bridge), 40e18, "bridge main-token balance after sendToBridge"); + + // ---------------------- receive xToken from the bridge + { + vm.expectRevert(IControllable.IncorrectZeroArgument.selector); + vm.prank(bridge); + xToken.takeFromBridge(address(0), 1e18); + + vm.expectRevert(IControllable.IncorrectZeroArgument.selector); + vm.prank(bridge); + xToken.takeFromBridge(user, 0); + + vm.expectRevert(IControllable.IncorrectMsgSender.selector); + vm.prank(user); + xToken.takeFromBridge(bridge, 1e18); + + vm.prank(bridge); + tokenA.approve(address(xToken), 40e18); + + vm.prank(bridge); + xToken.takeFromBridge(user, 40e18); + } + + assertEq(IERC20(address(xToken)).balanceOf(user), 100e18, "user xToken balance after takeFromBridge"); + assertEq( + IERC20(address(tokenA)).balanceOf(address(xToken)), 100e18, "locked main-token balance after takeFromBridge" + ); + assertEq(IERC20(address(tokenA)).balanceOf(user), 0, "user main-token balance after takeFromBridge"); + assertEq(IERC20(address(tokenA)).balanceOf(bridge), 0, "bridge main-token balance after takeFromBridge"); + } + + function testSetNameSymbol() public { + address multisig = platform.multisig(); + + assertEq(keccak256(bytes(IERC20Metadata(address(xToken)).name())), keccak256(bytes("xStability"))); + assertEq(keccak256(bytes(IERC20Metadata(address(xToken)).symbol())), keccak256(bytes("xSTBL"))); + + vm.expectRevert(IControllable.NotOperator.selector); + vm.prank(address(2)); + xToken.setName("NewName"); + + vm.prank(multisig); + xToken.setName("NewName"); + + assertEq(keccak256(bytes(IERC20Metadata(address(xToken)).name())), keccak256(bytes("NewName"))); + + vm.expectRevert(IControllable.NotOperator.selector); + vm.prank(address(2)); + xToken.setSymbol("NewSymbol"); + + vm.prank(multisig); + xToken.setSymbol("NewSymbol"); + + assertEq(keccak256(bytes(IERC20Metadata(address(xToken)).symbol())), keccak256(bytes("NewSymbol"))); + } + + //endregion --------------------- Tests + + //region --------------------- Helpers + function _setSlashingPenalty(IDAO daoToken, uint penalty_) internal { + address multisig = platform.multisig(); + + IDAO.DaoParams memory config = daoToken.config(); + config.exitPenalty = penalty_; + + vm.prank(multisig); + daoToken.updateConfig(config); + } + + function _createDAOInstance() internal returns (IDAO) { + IDAO.DaoParams memory p = IDAO.DaoParams({ + minimalPower: 4000e18, + exitPenalty: 50_00, + quorum: 20_00, + proposalThreshold: 10_00, + powerAllocationDelay: 86400 + }); + + Proxy proxy = new Proxy(); + proxy.initProxy(address(new DAO())); + IDAO token = IDAO(address(proxy)); + token.initialize(address(platform), address(xToken), address(xStaking), p, "Stability DAO", "STBL_DAO"); + return token; + } + //endregion --------------------- Helpers +} diff --git a/test/tokenomics/XTokenBridge.t.sol b/test/tokenomics/XTokenBridge.t.sol new file mode 100644 index 00000000..503e7997 --- /dev/null +++ b/test/tokenomics/XTokenBridge.t.sol @@ -0,0 +1,884 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {XToken} from "../../src/tokenomics/XToken.sol"; +import {BridgeTestLib} from "./libs/BridgeTestLib.sol"; +import {console, Test, Vm} from "forge-std/Test.sol"; +import {XStaking} from "../../src/tokenomics/XStaking.sol"; +import {XTokenBridge} from "../../src/tokenomics/XTokenBridge.sol"; +import {OptionsBuilder} from "@layerzerolabs/oapp-evm/contracts/oapp/libs/OptionsBuilder.sol"; +import {IXToken} from "../../src/interfaces/IXToken.sol"; +import {IControllable} from "../../src/interfaces/IControllable.sol"; +import {IPlatform} from "../../src/interfaces/IPlatform.sol"; +import {IOFTPausable} from "../../src/interfaces/IOFTPausable.sol"; +import {IXTokenBridge} from "../../src/interfaces/IXTokenBridge.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {TokenOFTAdapter} from "../../src/tokenomics/TokenOFTAdapter.sol"; +import {BridgedToken} from "../../src/tokenomics/BridgedToken.sol"; +import {XStaking} from "../../src/tokenomics/XStaking.sol"; +import {SonicConstantsLib} from "../../chains/sonic/SonicConstantsLib.sol"; +import {MessagingFee} from "@layerzerolabs/oft-evm/contracts/interfaces/IOFT.sol"; +import {Origin} from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppReceiver.sol"; +import {Proxy} from "../../src/core/proxy/Proxy.sol"; +import {IOAppReceiver} from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppReceiver.sol"; +import {IOAppComposer} from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppComposer.sol"; +import {MockXToken} from "../../src/test/MockXToken.sol"; +import {OFTComposeMsgCodec} from "@layerzerolabs/oft-evm/contracts/libs/OFTComposeMsgCodec.sol"; +import {ILayerZeroEndpointV2} from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; + +contract XTokenBridgeTest is Test { + using OptionsBuilder for bytes; + using SafeERC20 for IERC20; + + //region ------------------------------------- Constants, data types, variables + uint private constant SONIC_FORK_BLOCK = 52228979; // Oct-28-2025 01:14:21 PM +UTC + uint private constant AVALANCHE_FORK_BLOCK = 71037861; // Oct-28-2025 13:17:17 UTC + uint private constant PLASMA_FORK_BLOCK = 5398928; // Nov-5-2025 07:38:59 UTC + + /// @dev Gas limit for executor lzReceive calls + uint128 private constant GAS_LIMIT_LZRECEIVE = 100_000; + /// @dev Gas limit for executor lzCompose calls + uint128 private constant GAS_LIMIT_LZCOMPOSE = 150_000; + + TokenOFTAdapter internal adapter; + BridgedToken internal bridgedTokenAvalanche; + BridgedToken internal bridgedTokenPlasma; + + BridgeTestLib.ChainConfig internal sonic; + BridgeTestLib.ChainConfig internal avalanche; + BridgeTestLib.ChainConfig internal plasma; + + address private constant TEST_DELEGATOR = address(0x999); + + struct ChainResults { + uint balanceUserMainToken; + uint balanceUserXToken; + uint balanceOappMainToken; + uint balanceXTokenMainToken; + uint balanceUserEther; + uint balanceXTokenBridgedMainToken; + } + + struct Results { + ChainResults srcBefore; + ChainResults targetBefore; + ChainResults srcAfter; + ChainResults targetAfter; + uint nativeFee; + } + + //endregion ------------------------------------- Constants, data types, variables + + //region ------------------------------------- Constructor + constructor() { + { + uint forkSonic = vm.createFork(vm.envString("SONIC_RPC_URL"), SONIC_FORK_BLOCK); + uint forkAvalanche = vm.createFork(vm.envString("AVALANCHE_RPC_URL"), AVALANCHE_FORK_BLOCK); + uint forkPlasma = vm.createFork(vm.envString("PLASMA_RPC_URL"), PLASMA_FORK_BLOCK); + + sonic = BridgeTestLib.createConfigSonic(vm, forkSonic, TEST_DELEGATOR); + avalanche = BridgeTestLib.createConfigAvalanche(vm, forkAvalanche, TEST_DELEGATOR); + plasma = BridgeTestLib.createConfigPlasma(vm, forkPlasma, TEST_DELEGATOR); + } + + // ------------------- Create bridge for STBL + adapter = TokenOFTAdapter(BridgeTestLib.setupTokenOFTAdapterOnSonic(vm, sonic)); + bridgedTokenAvalanche = BridgedToken(BridgeTestLib.setupBridgedMainToken(vm, avalanche)); + bridgedTokenPlasma = BridgedToken(BridgeTestLib.setupBridgedMainToken(vm, plasma)); + + sonic.oapp = address(adapter); + avalanche.oapp = address(bridgedTokenAvalanche); + plasma.oapp = address(bridgedTokenPlasma); + + // ------------------- Upgrade xToken on sonic, deploy xToken on other chains + _upgradeSonicPlatform(); + avalanche.xToken = createXToken(avalanche); + plasma.xToken = createXToken(plasma); + + // ------------------- Create XTokenBridge + sonic.xTokenBridge = createXTokenBridge(sonic); + avalanche.xTokenBridge = createXTokenBridge(avalanche); + plasma.xTokenBridge = createXTokenBridge(plasma); + + _setXTokenBridge(sonic); + _setXTokenBridge(avalanche); + _setXTokenBridge(plasma); + + // ------------------- Set up STBL-bridges + BridgeTestLib.setUpSonicAvalanche(vm, sonic, avalanche); + BridgeTestLib.setUpSonicPlasma(vm, sonic, plasma); + BridgeTestLib.setUpAvalanchePlasma(vm, avalanche, plasma); + + // ------------------- Provide ether to address(this) to be able to pay fees + vm.selectFork(sonic.fork); + deal(address(this), 100 ether); + + vm.selectFork(plasma.fork); + deal(address(this), 100 ether); + + vm.selectFork(avalanche.fork); + deal(address(this), 100 ether); + } + + //endregion ------------------------------------- Constructor + + //region ------------------------------------- Unit tests + function testStorage() public pure { + bytes32 h = keccak256(abi.encode(uint(keccak256("erc7201:stability.XTokenBridge")) - 1)) & ~bytes32(uint(0xff)); + assertEq(h, 0x7331a1638fe957f8dc3395f52254374f52b3cbbdf185d4405a764a49dfb7f400, "storage hash"); + } + + function testViewSonic() public { + vm.selectFork(sonic.fork); + + IXTokenBridge xTokenBridge = IXTokenBridge(sonic.xTokenBridge); + assertEq(xTokenBridge.bridge(), sonic.oapp, "sonic: bridge"); + assertEq(xTokenBridge.xToken(), sonic.xToken, "sonic: xToken"); + } + + function testSetXTokenBridge() public { + vm.selectFork(sonic.fork); + + IXTokenBridge xTokenBridge = IXTokenBridge(sonic.xTokenBridge); + + uint32[] memory dstEids = new uint32[](2); + dstEids[0] = avalanche.endpointId; + dstEids[1] = plasma.endpointId; + address[] memory listXTokenBridges = new address[](2); + listXTokenBridges[0] = avalanche.xTokenBridge; + listXTokenBridges[1] = plasma.xTokenBridge; + + // ------------------- bad paths + vm.expectRevert(IControllable.NotGovernanceAndNotMultisig.selector); + vm.prank(address(0x1234)); + xTokenBridge.setXTokenBridge(new uint32[](0), new address[](0)); + + vm.expectRevert(IControllable.IncorrectArrayLength.selector); + vm.prank(sonic.multisig); + xTokenBridge.setXTokenBridge(dstEids, new address[](0)); + + vm.expectRevert(IControllable.IncorrectArrayLength.selector); + vm.prank(sonic.multisig); + xTokenBridge.setXTokenBridge(new uint32[](1), listXTokenBridges); + + // ------------------- good paths + assertEq(xTokenBridge.xTokenBridge(avalanche.endpointId), address(0), "before: avalanche bridge"); + assertEq(xTokenBridge.xTokenBridge(plasma.endpointId), address(0), "before: plasma bridge"); + + vm.prank(sonic.multisig); + xTokenBridge.setXTokenBridge(dstEids, listXTokenBridges); + + assertEq(xTokenBridge.xTokenBridge(avalanche.endpointId), avalanche.xTokenBridge, "after: avalanche bridge"); + assertEq(xTokenBridge.xTokenBridge(plasma.endpointId), plasma.xTokenBridge, "after: plasma bridge"); + + dstEids = new uint32[](1); + dstEids[0] = avalanche.endpointId; + + vm.prank(sonic.multisig); + xTokenBridge.setXTokenBridge(dstEids, new address[](1)); + + assertEq(xTokenBridge.xTokenBridge(avalanche.endpointId), address(0), "avalanche bridge is cleared"); + assertEq(xTokenBridge.xTokenBridge(plasma.endpointId), plasma.xTokenBridge, "after: plasma bridge"); + } + + function testSalvage() public { + vm.selectFork(sonic.fork); + address receiver = makeAddr("receiver"); + + IXTokenBridge xTokenBridge = IXTokenBridge(sonic.xTokenBridge); + IERC20 stbl = IERC20(IXToken(sonic.xToken).token()); + + // ------------------- send some STBL to the xTokenBridge + deal(address(stbl), address(this), 100e18); + stbl.approve(address(xTokenBridge), 100e18); + stbl.safeTransfer(address(xTokenBridge), 100e18); + + assertEq(stbl.balanceOf(address(xTokenBridge)), 100e18, "before: bridge STBL balance"); + assertEq(stbl.balanceOf(receiver), 0, "before: multisig STBL balance"); + + // ------------------- bad paths + vm.expectRevert(IControllable.NotGovernanceAndNotMultisig.selector); + vm.prank(address(0x1234)); + xTokenBridge.salvage(address(stbl), 70e18, receiver); + + // ------------------- good paths + vm.prank(sonic.multisig); + xTokenBridge.salvage(address(stbl), 70e18, receiver); + + assertEq(stbl.balanceOf(address(xTokenBridge)), 30e18, "after 1: bridge STBL balance"); + assertEq(stbl.balanceOf(receiver), 70e18, "after 1: receiver STBL balance"); + + vm.prank(sonic.multisig); + xTokenBridge.salvage(address(stbl), 0, receiver); + + assertEq(stbl.balanceOf(address(xTokenBridge)), 0, "after 2: bridge STBL balance"); + assertEq(stbl.balanceOf(receiver), 100e18, "after 2: receiver STBL balance"); + } + + function testSendBadPaths() public { + _setUpXTokenBridges(); + + // ------------------- provide xToken to the user + vm.selectFork(sonic.fork); + IXTokenBridge xTokenBridge = IXTokenBridge(sonic.xTokenBridge); + + deal(SonicConstantsLib.TOKEN_STBL, address(this), 100e18); + + IERC20(SonicConstantsLib.TOKEN_STBL).approve(sonic.xToken, 100e18); + IXToken(sonic.xToken).enter(100e18); + + // ------------------- incorrect value + { + uint snapshot = vm.snapshotState(); + + bytes memory options = xTokenBridge.buildOptions(GAS_LIMIT_LZRECEIVE, 0, 0, GAS_LIMIT_LZCOMPOSE, 0); + // bytes memory options = OptionsBuilder.newOptions().addExecutorLzReceiveOption(GAS_LIMIT_LZRECEIVE, 0) + // .addExecutorLzComposeOption(0, GAS_LIMIT_LZCOMPOSE, 0); + MessagingFee memory msgFee = + IXTokenBridge(sonic.xTokenBridge).quoteSend(avalanche.endpointId, 1e18, options); + + vm.expectRevert(IXTokenBridge.IncorrectNativeValue.selector); + xTokenBridge.send{value: msgFee.nativeFee + 1}(avalanche.endpointId, 1e18, msgFee, options); + vm.revertToState(snapshot); + } + + // ------------------- zero amount + { + uint snapshot = vm.snapshotState(); + + bytes memory options = OptionsBuilder.newOptions().addExecutorLzReceiveOption(GAS_LIMIT_LZRECEIVE, 0) + .addExecutorLzComposeOption(0, GAS_LIMIT_LZCOMPOSE, 0); + MessagingFee memory msgFee = IXTokenBridge(sonic.xTokenBridge).quoteSend(avalanche.endpointId, 0, options); + + vm.expectRevert(IXTokenBridge.ZeroAmount.selector); + xTokenBridge.send{value: msgFee.nativeFee}(avalanche.endpointId, 0, msgFee, options); + vm.revertToState(snapshot); + } + + // ------------------- sender is paused + { + uint snapshot = vm.snapshotState(); + vm.prank(sonic.multisig); + IOFTPausable(sonic.oapp).setPaused(address(this), true); + + bytes memory options = OptionsBuilder.newOptions().addExecutorLzReceiveOption(GAS_LIMIT_LZRECEIVE, 0) + .addExecutorLzComposeOption(0, GAS_LIMIT_LZCOMPOSE, 0); + MessagingFee memory msgFee = + IXTokenBridge(sonic.xTokenBridge).quoteSend(avalanche.endpointId, 1e18, options); + + vm.expectRevert(IXTokenBridge.SenderPaused.selector); + xTokenBridge.send{value: msgFee.nativeFee}(avalanche.endpointId, 1e18, msgFee, options); + vm.revertToState(snapshot); + } + + // ------------------- chain not supported + { + uint snapshot = vm.snapshotState(); + + bytes memory options = OptionsBuilder.newOptions().addExecutorLzReceiveOption(GAS_LIMIT_LZRECEIVE, 0) + .addExecutorLzComposeOption(0, GAS_LIMIT_LZCOMPOSE, 0); + MessagingFee memory msgFee = + IXTokenBridge(sonic.xTokenBridge).quoteSend(avalanche.endpointId, 1e18, options); + + // struct MessagingFee {uint256 nativeFee; uint256 lzTokenFee; } + vm.expectRevert(IXTokenBridge.ChainNotSupported.selector); + xTokenBridge.send{value: msgFee.nativeFee}(98013078, 1e18, msgFee, options); // 98013078 is not valid endpointId + vm.revertToState(snapshot); + } + } + + function testSendIncorrectAmount() public { + vm.selectFork(sonic.fork); + + // ------------------- setup an instance of xTokenBridge with mocked xToken + Proxy xTokenBridgeProxy = new Proxy(); + xTokenBridgeProxy.initProxy(address(new XTokenBridge(sonic.endpoint))); + + MockXToken mockedXToken = new MockXToken(SonicConstantsLib.TOKEN_STBL, 50e18); + deal(SonicConstantsLib.TOKEN_STBL, address(mockedXToken), 50e18); + + XTokenBridge(address(xTokenBridgeProxy)).initialize(address(sonic.platform), sonic.oapp, address(mockedXToken)); + + // ------------------- provide xToken to the user + vm.selectFork(sonic.fork); + IXTokenBridge xTokenBridge = IXTokenBridge(address(xTokenBridgeProxy)); + + deal(SonicConstantsLib.TOKEN_STBL, address(this), 100e18); + + IERC20(SonicConstantsLib.TOKEN_STBL).approve(sonic.xToken, 100e18); + IXToken(sonic.xToken).enter(100e18); + + // ------------------- chain not supported + vm.expectRevert(); // IXTokenBridge.IncorrectAmountReceivedFromXToken.selector); + xTokenBridge.send{value: 1e18}(avalanche.endpointId, 100e18, MessagingFee({nativeFee: 1e18, lzTokenFee: 0}), ""); + } + + function testComposeBadPaths() public { + _setUpXTokenBridges(); + + vm.selectFork(sonic.fork); + + vm.expectRevert(IXTokenBridge.UnauthorizedSender.selector); + vm.prank(makeAddr("some wrong sender")); // (!) + IOAppComposer(sonic.xTokenBridge) + .lzCompose( + sonic.oapp, + bytes32(0), + "", // compose message + address(0), // executor + "" // extraData + ); + + vm.expectRevert(IXTokenBridge.UntrustedOApp.selector); + vm.prank(sonic.endpoint); + IOAppComposer(sonic.xTokenBridge) + .lzCompose( + makeAddr("some other oapp"), // (!) + bytes32(0), + "", // compose message + address(0), // executor + "" // extraData + ); + } + + function testComposeInvalidSenderXTokenBridge() public { + _setUpXTokenBridges(); + + // --------------- mint xToken on Sonic + vm.selectFork(sonic.fork); + deal(SonicConstantsLib.TOKEN_STBL, address(this), 100e18); + + IERC20(SonicConstantsLib.TOKEN_STBL).approve(sonic.xToken, 100e18); + IXToken(sonic.xToken).enter(100e18); + + // --------------- send xToken on sonic + vm.selectFork(sonic.fork); + + bytes memory options = OptionsBuilder.newOptions().addExecutorLzReceiveOption(GAS_LIMIT_LZRECEIVE, 0) + .addExecutorLzComposeOption(0, GAS_LIMIT_LZCOMPOSE, 0); + MessagingFee memory msgFee = IXTokenBridge(sonic.xTokenBridge).quoteSend(avalanche.endpointId, 1e18, options); + + vm.recordLogs(); + IXTokenBridge(sonic.xTokenBridge).send{value: msgFee.nativeFee}(avalanche.endpointId, 1e18, msgFee, options); + (bytes memory message,) = BridgeTestLib._extractSendMessage(vm.getRecordedLogs()); + + // --------------- Simulate message receiving on avalanche + vm.selectFork(avalanche.fork); + + { + vm.recordLogs(); + vm.prank(avalanche.endpoint); + IOAppReceiver(avalanche.oapp) + .lzReceive( + Origin({srcEid: sonic.endpointId, sender: bytes32(uint(uint160(address(sonic.oapp)))), nonce: 1}), + bytes32(uint(1)), + message, + address(0), // executor + "" // extraData + ); + } + + { + (,, bytes memory composeMessage) = BridgeTestLib._extractComposeMessage(vm.getRecordedLogs()); + + // now we have composeMessage with stored sonic.xTokenBridge inside + // let's check value of sonic.xTokenBridge on avalanche side to simulate error + + uint32[] memory dstEids = new uint32[](1); + dstEids[0] = sonic.endpointId; + address[] memory addrs = new address[](1); + addrs[0] = makeAddr("wrong sonic.xTokenBridge address"); + + vm.prank(avalanche.multisig); + IXTokenBridge(avalanche.xTokenBridge).setXTokenBridge(dstEids, addrs); + + vm.expectRevert(IXTokenBridge.InvalidSenderXTokenBridge.selector); + vm.prank(avalanche.endpoint); + IOAppComposer(avalanche.xTokenBridge) + .lzCompose(avalanche.oapp, bytes32(uint(2)), composeMessage, address(0), ""); + } + } + + function testComposeZeroValues() public { + _setUpXTokenBridges(); + vm.selectFork(avalanche.fork); + + { + bytes memory composeMessage = OFTComposeMsgCodec.encode( + 0, + sonic.endpointId, + 0, + // 0x[composeFrom][composeMsg], see OFTComposeMsgCodec.encode + abi.encodePacked(OFTComposeMsgCodec.addressToBytes32(sonic.xTokenBridge), abi.encode(address(this))) + ); + + vm.expectRevert(IXTokenBridge.ZeroAmount.selector); + vm.prank(avalanche.endpoint); + IOAppComposer(avalanche.xTokenBridge) + .lzCompose(avalanche.oapp, bytes32(uint(2)), composeMessage, address(0), ""); + } + + { + bytes memory composeMessage = OFTComposeMsgCodec.encode( + 0, + sonic.endpointId, + 1e18, + // 0x[composeFrom][composeMsg], see OFTComposeMsgCodec.encode + abi.encodePacked(OFTComposeMsgCodec.addressToBytes32(sonic.xTokenBridge), abi.encode(address(0))) + ); + + vm.expectRevert(IXTokenBridge.IncorrectReceiver.selector); + vm.prank(avalanche.endpoint); + IOAppComposer(avalanche.xTokenBridge) + .lzCompose(avalanche.oapp, bytes32(uint(2)), composeMessage, address(0), ""); + } + } + + //endregion ------------------------------------- Unit tests + + //region ------------------------------------- Send xToken between chains + function testSendXTokenFromSonicToPlasma() public { + _setUpXTokenBridges(); + + // --------------- mint xToken on Sonic + vm.selectFork(sonic.fork); + deal(SonicConstantsLib.TOKEN_STBL, address(this), 100e18); + + IERC20(SonicConstantsLib.TOKEN_STBL).approve(sonic.xToken, 100e18); + IXToken(sonic.xToken).enter(100e18); + + // --------------- send xToken from Sonic to Plasma + Results memory r1 = _testSendXToken(sonic, plasma, 70e18, 0); + + assertEq(r1.srcBefore.balanceUserXToken, 100e18, "sonic: user xToken before"); + assertEq(r1.srcAfter.balanceUserXToken, 30e18, "sonic: user xToken after"); + assertEq(r1.targetBefore.balanceUserXToken, 0, "plasma: user xToken before"); + assertEq(r1.targetAfter.balanceUserXToken, 70e18, "plasma: user xToken after"); + + assertEq(r1.srcBefore.balanceXTokenBridgedMainToken, 0, "sonic: xTokenBridge main-token before"); + assertEq(r1.srcAfter.balanceXTokenBridgedMainToken, 0, "sonic: xTokenBridge main-token after"); + assertEq(r1.targetBefore.balanceXTokenBridgedMainToken, 0, "plasma: xTokenBridge main-token before"); + assertEq(r1.targetAfter.balanceXTokenBridgedMainToken, 0, "plasma: xTokenBridge main-token after"); + + assertEq( + r1.srcAfter.balanceXTokenMainToken, + r1.srcBefore.balanceXTokenMainToken - 70e18, + "sonic: xToken main-token after" + ); + assertEq(r1.targetAfter.balanceXTokenMainToken, 70e18, "plasma: main-token staked to xToken"); + + assertEq(r1.srcAfter.balanceOappMainToken, 70e18, "sonic: expected amount of locked main-token in the bridge"); + + // --------------- send xToken from Sonic to Plasma 2 + Results memory r2 = _testSendXToken(sonic, plasma, 30e18, 1); + + assertEq(r2.srcBefore.balanceUserXToken, 30e18, "sonic: user xToken before 2"); + assertEq(r2.srcAfter.balanceUserXToken, 0, "sonic: user xToken after 2"); + assertEq(r2.targetBefore.balanceUserXToken, 70e18, "plasma: user xToken before 2"); + assertEq(r2.targetAfter.balanceUserXToken, 100e18, "plasma: user xToken after 2"); + + assertEq(r2.srcBefore.balanceXTokenBridgedMainToken, 0, "sonic: xTokenBridge main-token before 2"); + assertEq(r2.srcAfter.balanceXTokenBridgedMainToken, 0, "sonic: xTokenBridge main-token after 2"); + assertEq(r2.targetBefore.balanceXTokenBridgedMainToken, 0, "plasma: xTokenBridge main-token before 2"); + assertEq(r2.targetAfter.balanceXTokenBridgedMainToken, 0, "plasma: xTokenBridge main-token after 2"); + + assertEq( + r2.srcAfter.balanceXTokenMainToken, + r2.srcBefore.balanceXTokenMainToken - 30e18, + "sonic: xToken main-token after 2" + ); + assertEq(r2.targetAfter.balanceXTokenMainToken, 100e18, "plasma: main-token staked to xToken 2"); + + assertEq( + r2.srcAfter.balanceOappMainToken, 100e18, "sonic: expected amount of locked main-token in the bridge 2" + ); + + // --------------- send xToken back from Plasma to Sonic + Results memory r3 = _testSendXToken(plasma, sonic, 100e18, 2); + + assertEq(r3.srcBefore.balanceUserXToken, 100e18, "plasma: user xToken before 3"); + assertEq(r3.srcAfter.balanceUserXToken, 0, "plasma: user xToken after 3"); + assertEq(r3.targetBefore.balanceUserXToken, 0, "sonic: user xToken before 3"); + assertEq(r3.targetAfter.balanceUserXToken, 100e18, "sonic: user xToken after 3"); + + assertEq(r3.srcBefore.balanceXTokenBridgedMainToken, 0, "plasma: xTokenBridge main-token before 3"); + assertEq(r3.srcAfter.balanceXTokenBridgedMainToken, 0, "plasma: xTokenBridge main-token after 3"); + assertEq(r3.targetBefore.balanceXTokenBridgedMainToken, 0, "sonic: xTokenBridge main-token before 3"); + assertEq(r3.targetAfter.balanceXTokenBridgedMainToken, 0, "sonic: xTokenBridge main-token after 3"); + + assertEq(r3.srcAfter.balanceXTokenMainToken, 0, "plasma: xToken main-token after 3"); + assertEq( + r3.targetAfter.balanceXTokenMainToken, + r1.srcBefore.balanceXTokenMainToken, + "sonic: all main-token were returned back to xToken" + ); + + assertEq(r3.srcAfter.balanceOappMainToken, 0, "plasma: expected amount of locked main-token in the bridge 3"); + } + + function testSendXTokenFromAvalancheToPlasma() public { + _setUpXTokenBridges(); + + // --------------- mint xToken on Sonic + vm.selectFork(sonic.fork); + deal(SonicConstantsLib.TOKEN_STBL, address(this), 100e18); + + IERC20(SonicConstantsLib.TOKEN_STBL).approve(sonic.xToken, 100e18); + IXToken(sonic.xToken).enter(100e18); + + // --------------- send xToken from Sonic to Avalanche + _testSendXToken(sonic, avalanche, 100e18, 1345); + + // --------------- send xToken from avalanche to Plasma + Results memory r1 = _testSendXToken(avalanche, plasma, 70e18, 0); + + assertEq(r1.srcBefore.balanceUserXToken, 100e18, "avalanche: user xToken before"); + assertEq(r1.srcAfter.balanceUserXToken, 30e18, "avalanche: user xToken after"); + assertEq(r1.targetBefore.balanceUserXToken, 0, "plasma: user xToken before"); + assertEq(r1.targetAfter.balanceUserXToken, 70e18, "plasma: user xToken after"); + + assertEq(r1.srcBefore.balanceXTokenBridgedMainToken, 0, "avalanche: xTokenBridge main-token before"); + assertEq(r1.srcAfter.balanceXTokenBridgedMainToken, 0, "avalanche: xTokenBridge main-token after"); + assertEq(r1.targetBefore.balanceXTokenBridgedMainToken, 0, "plasma: xTokenBridge main-token before"); + assertEq(r1.targetAfter.balanceXTokenBridgedMainToken, 0, "plasma: xTokenBridge main-token after"); + + assertEq( + r1.srcAfter.balanceXTokenMainToken, + r1.srcBefore.balanceXTokenMainToken - 70e18, + "avalanche: xToken main-token after" + ); + assertEq(r1.targetAfter.balanceXTokenMainToken, 70e18, "plasma: main-token staked to xToken"); + + assertEq(r1.srcAfter.balanceOappMainToken, 0, "avalanche: expected amount of locked STBL in the bridge"); + + // --------------- send xToken from avalanche to Plasma 2 + Results memory r2 = _testSendXToken(avalanche, plasma, 30e18, 1); + + assertEq(r2.srcBefore.balanceUserXToken, 30e18, "avalanche: user xToken before 2"); + assertEq(r2.srcAfter.balanceUserXToken, 0, "avalanche: user xToken after 2"); + assertEq(r2.targetBefore.balanceUserXToken, 70e18, "plasma: user xToken before 2"); + assertEq(r2.targetAfter.balanceUserXToken, 100e18, "plasma: user xToken after 2"); + + assertEq(r2.srcBefore.balanceXTokenBridgedMainToken, 0, "avalanche: xTokenBridge main-token before 2"); + assertEq(r2.srcAfter.balanceXTokenBridgedMainToken, 0, "avalanche: xTokenBridge main-token after 2"); + assertEq(r2.targetBefore.balanceXTokenBridgedMainToken, 0, "plasma: xTokenBridge main-token before 2"); + assertEq(r2.targetAfter.balanceXTokenBridgedMainToken, 0, "plasma: xTokenBridge main-token after 2"); + + assertEq( + r2.srcAfter.balanceXTokenMainToken, + r2.srcBefore.balanceXTokenMainToken - 30e18, + "avalanche: xToken main-token after 2" + ); + assertEq(r2.targetAfter.balanceXTokenMainToken, 100e18, "plasma: main-token staked to xToken 2"); + + assertEq(r2.srcAfter.balanceOappMainToken, 0, "avalanche: expected amount of locked main-token in the bridge 2"); + + // --------------- send xToken back from Plasma to avalanche + Results memory r3 = _testSendXToken(plasma, avalanche, 100e18, 2); + + assertEq(r3.srcBefore.balanceUserXToken, 100e18, "plasma: user xToken before 3"); + assertEq(r3.srcAfter.balanceUserXToken, 0, "plasma: user xToken after 3"); + assertEq(r3.targetBefore.balanceUserXToken, 0, "avalanche: user xToken before 3"); + assertEq(r3.targetAfter.balanceUserXToken, 100e18, "avalanche: user xToken after 3"); + + assertEq(r3.srcBefore.balanceXTokenBridgedMainToken, 0, "plasma: xTokenBridge main-token before 3"); + assertEq(r3.srcAfter.balanceXTokenBridgedMainToken, 0, "plasma: xTokenBridge main-token after 3"); + assertEq(r3.targetBefore.balanceXTokenBridgedMainToken, 0, "avalanche: xTokenBridge main-token before 3"); + assertEq(r3.targetAfter.balanceXTokenBridgedMainToken, 0, "avalanche: xTokenBridge main-token after 3"); + + assertEq(r3.srcAfter.balanceXTokenMainToken, 0, "plasma: xToken main-token after 3"); + assertEq( + r3.targetAfter.balanceXTokenMainToken, + r1.srcBefore.balanceXTokenMainToken, + "avalanche: all STBL were returned back to xToken" + ); + + assertEq(r3.srcAfter.balanceOappMainToken, 0, "plasma: expected amount of locked main-token in the bridge 3"); + } + + function testReceiveThroughEndpoint() public { + _setUpXTokenBridges(); + + // --------------- provide xToken to the user + vm.selectFork(sonic.fork); + + deal(SonicConstantsLib.TOKEN_STBL, address(this), 100e18); + IERC20(SonicConstantsLib.TOKEN_STBL).approve(sonic.xToken, 100e18); + IXToken(sonic.xToken).enter(100e18); + + // --------------- send xToken on src + bytes memory options = OptionsBuilder.newOptions().addExecutorLzReceiveOption(GAS_LIMIT_LZRECEIVE, 0) + .addExecutorLzComposeOption(0, GAS_LIMIT_LZCOMPOSE, 0); + MessagingFee memory msgFee = IXTokenBridge(sonic.xTokenBridge).quoteSend(plasma.endpointId, 1e18, options); + + vm.recordLogs(); + IXTokenBridge(sonic.xTokenBridge).send{value: msgFee.nativeFee}(plasma.endpointId, 1e18, msgFee, options); + Vm.Log[] memory logs = vm.getRecordedLogs(); + // decode Layer Zero's event PacketSent + (bytes memory message, bytes32 guidId_) = BridgeTestLib._extractSendMessage(logs); + // decode XTokenBridge's event XTokenSent + (,,,, bytes32 guidId, uint64 nonce,) = BridgeTestLib._extractXTokenSentMessage(logs); + assertEq(guidId, guidId_, "XTokenSent has correct guid"); + + // --------------- Receive message on plasma side + vm.selectFork(plasma.fork); + + Origin memory origin = + Origin({srcEid: sonic.endpointId, sender: bytes32(uint(uint160(address(sonic.oapp)))), nonce: nonce}); + + vm.prank(plasma.receiveLib); + ILayerZeroEndpointV2(plasma.endpoint).verify(origin, plasma.oapp, keccak256(abi.encodePacked(guidId_, message))); + + { + bool isVerifiable = ILayerZeroEndpointV2(plasma.endpoint).verifiable(origin, plasma.oapp); + require(isVerifiable, "Message not verifiable yet"); + + bytes32 inboundPayloadHash = ILayerZeroEndpointV2(plasma.endpoint) + .inboundPayloadHash(plasma.oapp, sonic.endpointId, bytes32(uint(uint160(address(sonic.oapp)))), nonce); + assertEq(inboundPayloadHash, keccak256(abi.encodePacked(guidId_, message))); + + uint64 currentInboundNonce = ILayerZeroEndpointV2(plasma.endpoint) + .inboundNonce(plasma.oapp, sonic.endpointId, bytes32(uint(uint160(address(sonic.oapp))))); + assertEq(currentInboundNonce, 1, "Inbound nonce should be 1 before lzReceive (and 0 initially)"); + } + + vm.recordLogs(); + vm.prank(plasma.executor); + ILayerZeroEndpointV2(plasma.endpoint).lzReceive(origin, plasma.oapp, guidId_, message, ""); + (,, bytes memory composeMessage) = BridgeTestLib._extractComposeMessage(vm.getRecordedLogs()); + + vm.prank(plasma.endpoint); + IOAppComposer(plasma.xTokenBridge).lzCompose(plasma.oapp, guidId_, composeMessage, address(0), ""); + + assertEq(IERC20(plasma.xToken).balanceOf(address(this)), 1e18, "user should receive 1e18 xToken on plasma"); + } + + //endregion ------------------------------------- Send xToken between chains + + //region ------------------------------------- Unit tests + function _testSendXToken( + BridgeTestLib.ChainConfig memory src, + BridgeTestLib.ChainConfig memory dest, + uint amount_, + uint guidId_ + ) internal returns (Results memory r) { + // --------------- initial state on src + vm.selectFork(dest.fork); + r.targetBefore = getBalances(dest, address(this)); + + // --------------- send xToken on src + vm.selectFork(src.fork); + r.srcBefore = getBalances(src, address(this)); + + bytes memory options = OptionsBuilder.newOptions().addExecutorLzReceiveOption(GAS_LIMIT_LZRECEIVE, 0) + .addExecutorLzComposeOption(0, GAS_LIMIT_LZCOMPOSE, 0); + MessagingFee memory msgFee = IXTokenBridge(src.xTokenBridge).quoteSend(dest.endpointId, amount_, options); + + vm.recordLogs(); + IXTokenBridge(src.xTokenBridge).send{value: msgFee.nativeFee}(dest.endpointId, amount_, msgFee, options); + (bytes memory message,) = BridgeTestLib._extractSendMessage(vm.getRecordedLogs()); + + // --------------- Simulate message receiving on dest + vm.selectFork(dest.fork); + + Origin memory origin = + Origin({srcEid: src.endpointId, sender: bytes32(uint(uint160(address(src.oapp)))), nonce: 1}); + + // --------------- lzReceive + { + uint gasBefore = gasleft(); + vm.recordLogs(); + vm.prank(dest.endpoint); + IOAppReceiver(dest.oapp) + .lzReceive( + origin, + bytes32(guidId_), // guid: actual value doesn't matter + message, + address(0), // executor + "" // extraData + ); + assertLt(gasBefore - gasleft(), GAS_LIMIT_LZRECEIVE, "lzReceive gas limit exceeded"); + console.log("gasBefore - gasleft() (lzReceive):", gasBefore - gasleft()); + } + + // --------------- lzCompose + + // see comment from OFTCore: + // @dev Stores the lzCompose payload that will be executed in a separate tx. + // Standardizes functionality for executing arbitrary contract invocation on some non-evm chains. + // @dev The off-chain executor will listen and process the msg based on the src-chain-callers compose options passed. + // @dev The index is used when a OApp needs to compose multiple msgs on lzReceive. + // For default OFT implementation there is only 1 compose msg per lzReceive, thus its always 0. + // endpoint.sendCompose(toAddress, _guid, 0 /* the index of the composed message*/, composeMsg); + // interface IMessagingComposer { + // event ComposeSent(address from, address to, bytes32 guid, uint16 index, bytes message); + + { + (address from, address to, bytes memory composeMessage) = + BridgeTestLib._extractComposeMessage(vm.getRecordedLogs()); + uint gasBefore = gasleft(); + vm.recordLogs(); + vm.prank(dest.endpoint); + IOAppComposer(dest.xTokenBridge) + .lzCompose( + dest.oapp, + bytes32(guidId_), // guid: actual value doesn't matter + composeMessage, + address(0), // executor + "" // extraData + ); + assertLt(gasBefore - gasleft(), GAS_LIMIT_LZCOMPOSE, "lzCompoze gas limit exceeded"); + console.log("gasBefore - gasleft() (compose):", gasBefore - gasleft()); + + assertEq(from, dest.oapp, "invalid compose from"); + assertEq(to, address(dest.xTokenBridge), "invalid compose to"); + } + + r.targetAfter = getBalances(dest, address(this)); + + // --------------- src + vm.selectFork(src.fork); + r.srcAfter = getBalances(src, address(this)); + + // showResults(r); + // + // console.log("user", address(this)); + // console.log("src.xToken", src.xToken); + // console.log("src.oapp", src.oapp); + // console.log("src.xTokenBridge", src.xTokenBridge); + // console.log("src.STBL", IXSTBL(src.xToken).STBL()); + // + // vm.selectFork(dest.fork); + // console.log("dest.xToken", dest.xToken); + // console.log("dest.oapp", dest.oapp); + // console.log("dest.xTokenBridge", dest.xTokenBridge); + // console.log("dest.STBL", IXSTBL(dest.xToken).STBL()); + } + + //endregion ------------------------------------- Unit tests + + //region ------------------------------------- Internal utils + function getBalances( + BridgeTestLib.ChainConfig memory chain, + address user + ) internal view returns (ChainResults memory results) { + IERC20 stbl = IERC20(IXToken(chain.xToken).token()); + + results.balanceUserMainToken = stbl.balanceOf(user); + results.balanceUserXToken = IERC20(chain.xToken).balanceOf(user); + results.balanceOappMainToken = stbl.balanceOf(chain.oapp); + results.balanceXTokenMainToken = stbl.balanceOf(chain.xToken); + results.balanceUserEther = user.balance; + results.balanceXTokenBridgedMainToken = stbl.balanceOf(chain.xTokenBridge); + } + + function createXToken(BridgeTestLib.ChainConfig memory chain) internal returns (address) { + vm.selectFork(chain.fork); + + Proxy xStakingProxy = new Proxy(); + xStakingProxy.initProxy(address(new XStaking())); + + Proxy xTokenProxy = new Proxy(); + xTokenProxy.initProxy(address(new XToken())); + + XToken(address(xTokenProxy)) + .initialize( + address(chain.platform), + chain.oapp, + address(xStakingProxy), + address(0), // revenue router is not used in the tests + "xStability", + "xSTBL" + ); + + XStaking(address(xStakingProxy)).initialize(address(chain.platform), address(xTokenProxy)); + + return address(xTokenProxy); + } + + function createXTokenBridge(BridgeTestLib.ChainConfig memory chain) internal returns (address) { + vm.selectFork(chain.fork); + + Proxy xTokenBridgeProxy = new Proxy(); + xTokenBridgeProxy.initProxy(address(new XTokenBridge(chain.endpoint))); + + XTokenBridge(address(xTokenBridgeProxy)).initialize(address(chain.platform), chain.oapp, chain.xToken); + + return address(xTokenBridgeProxy); + } + + function showResults(Results memory r) internal pure { + showChainResults("src.before", r.srcBefore); + showChainResults("target.before", r.targetBefore); + showChainResults("src.after", r.srcAfter); + showChainResults("target.after", r.targetAfter); + } + + function showChainResults(string memory label, ChainResults memory r) internal pure { + console.log("------------------ %s ------------------", label); + console.log("balanceUserMainToken", r.balanceUserMainToken); + console.log("balanceUserXToken", r.balanceUserXToken); + console.log("balanceOappMainToken", r.balanceOappMainToken); + console.log("balanceXTokenMainToken", r.balanceXTokenMainToken); + console.log("balanceUserEther", r.balanceUserEther); + console.log("balanceXTokenBridgedMainToken", r.balanceXTokenBridgedMainToken); + } + + function _setXTokenBridge( + BridgeTestLib.ChainConfig memory chain, + BridgeTestLib.ChainConfig memory c1, + BridgeTestLib.ChainConfig memory c2 + ) internal { + vm.selectFork(chain.fork); + + uint32[] memory dstEids = new uint32[](2); + dstEids[0] = c1.endpointId; + dstEids[1] = c2.endpointId; + address[] memory bridges = new address[](2); + bridges[0] = c1.xTokenBridge; + bridges[1] = c2.xTokenBridge; + + vm.prank(chain.multisig); + IXTokenBridge(chain.xTokenBridge).setXTokenBridge(dstEids, bridges); + } + + function _setXTokenBridge(BridgeTestLib.ChainConfig memory chain) internal { + vm.selectFork(chain.fork); + vm.prank(chain.multisig); + IXToken(chain.xToken).setBridge(chain.xTokenBridge, true); + } + + function _setUpXTokenBridges() internal { + _setXTokenBridge(sonic, avalanche, plasma); + _setXTokenBridge(avalanche, sonic, plasma); + _setXTokenBridge(plasma, sonic, avalanche); + } + + //endregion ------------------------------------- Internal utils + + //region ------------------------------------- Helpers + function _upgradeSonicPlatform() internal { + vm.selectFork(sonic.fork); + rewind(1 days); + + IPlatform platform = IPlatform(SonicConstantsLib.PLATFORM); + + address[] memory proxies = new address[](1); + address[] memory implementations = new address[](1); + + proxies[0] = SonicConstantsLib.TOKEN_XSTBL; + implementations[0] = address(new XToken()); + + // vm.startPrank(SonicConstantsLib.MULTISIG); + // platform.cancelUpgrade(); + + vm.startPrank(SonicConstantsLib.MULTISIG); + platform.announcePlatformUpgrade("2025.10.02-alpha", proxies, implementations); + + skip(1 days); + platform.upgrade(); + vm.stopPrank(); + } + //endregion ------------------------------------- Helpers +} diff --git a/test/tokenomics/libs/BridgeTestLib.sol b/test/tokenomics/libs/BridgeTestLib.sol new file mode 100644 index 00000000..9141edbb --- /dev/null +++ b/test/tokenomics/libs/BridgeTestLib.sol @@ -0,0 +1,591 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {console, Vm} from "forge-std/Test.sol"; +import {BridgedToken} from "../../../src/tokenomics/BridgedToken.sol"; +import {TokenOFTAdapter} from "../../../src/tokenomics/TokenOFTAdapter.sol"; +import {IPlatform} from "../../../src/interfaces/IPlatform.sol"; +import {IOAppCore} from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppCore.sol"; +import {SonicConstantsLib} from "../../../chains/sonic/SonicConstantsLib.sol"; +import {Proxy} from "../../../src/core/proxy/Proxy.sol"; +import {AvalancheConstantsLib} from "../../../chains/avalanche/AvalancheConstantsLib.sol"; +// import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +// import {OFTMsgCodec} from "@layerzerolabs/oft-evm/contracts/libs/OFTMsgCodec.sol"; +import {ILayerZeroEndpointV2} from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; +import {SetConfigParam} from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/IMessageLibManager.sol"; +import {ExecutorConfig} from "@layerzerolabs/lz-evm-messagelib-v2/contracts/SendLibBase.sol"; +import {UlnConfig} from "@layerzerolabs/lz-evm-messagelib-v2/contracts/uln/UlnBase.sol"; +// import {InboundPacket, PacketDecoder} from "@layerzerolabs/lz-evm-protocol-v2/../oapp/contracts/precrime/libs/Packet.sol"; +// import {PacketV1Codec} from "@layerzerolabs/lz-evm-protocol-v2/contracts/messagelib/libs/PacketV1Codec.sol"; +import {PlasmaConstantsLib} from "../../../chains/plasma/PlasmaConstantsLib.sol"; + +/// @notice Auxiliary data types and utils to test STBL-bridge related functionality +library BridgeTestLib { + /// @dev Set to 0 for immediate switch, or block number for gradual migration + uint private constant GRACE_PERIOD = 0; + uint32 internal constant CONFIG_TYPE_EXECUTOR = 1; + uint32 internal constant CONFIG_TYPE_ULN = 2; + + uint32 internal constant MAX_MESSAGE_SIZE = 256; + + // --------------- Confirmations: send >= receive, see https://docs.layerzero.network/v2/developers/evm/configuration/dvn-executor-config + + /// @dev Minimum block confirmations to wait on Sonic + uint64 internal constant MIN_BLOCK_CONFIRMATIONS_SEND_SONIC = 15; + + /// @dev Minimum block confirmations required on Avalanche + uint64 internal constant MIN_BLOCK_CONFIRMATIONS_RECEIVE_TARGET = 10; + + /// @dev Minimum block confirmations to wait on Avalanche + uint64 internal constant MIN_BLOCK_CONFIRMATIONS_SEND_TARGET = 15; + + /// @dev Minimum block confirmations required on Sonic + uint64 internal constant MIN_BLOCK_CONFIRMATIONS_RECEIVE_SONIC = 10; + + /// @dev By default shared decimals (min decimals at all chains) is 6 for STBL + uint internal constant SHARED_DECIMALS = 6; + + struct ChainConfig { + uint fork; + address multisig; + + /// @notice main-token-bridge + address oapp; + address xToken; + + uint32 endpointId; + address endpoint; + address sendLib; + address receiveLib; + address platform; + address executor; + + address xTokenBridge; + address delegator; + } + + //region ------------------------------------- Create contracts + function setupBridgedMainToken(Vm vm, BridgeTestLib.ChainConfig memory chain) internal returns (address) { + vm.selectFork(chain.fork); + + Proxy proxy = new Proxy(); + proxy.initProxy(address(new BridgedToken(chain.endpoint))); + BridgedToken bridgedMainToken = BridgedToken(address(proxy)); + bridgedMainToken.initialize(address(chain.platform), "Stability STBL", "STBL", chain.delegator); + + return address(bridgedMainToken); + } + + function setupTokenOFTAdapterOnSonic(Vm vm, BridgeTestLib.ChainConfig memory sonic) internal returns (address) { + vm.selectFork(sonic.fork); + + Proxy proxy = new Proxy(); + proxy.initProxy(address(new TokenOFTAdapter(SonicConstantsLib.TOKEN_STBL, sonic.endpoint))); + TokenOFTAdapter tokenOFTAdapter = TokenOFTAdapter(address(proxy)); + tokenOFTAdapter.initialize(address(sonic.platform), sonic.delegator); + + return address(tokenOFTAdapter); + } + + //endregion ------------------------------------- Create contracts + + //region ------------------------------------- Chains + function createConfigSonic( + Vm vm, + uint forkId, + address delegator + ) internal returns (BridgeTestLib.ChainConfig memory) { + vm.selectFork(forkId); + return BridgeTestLib.ChainConfig({ + fork: forkId, + multisig: IPlatform(SonicConstantsLib.PLATFORM).multisig(), + oapp: address(0), // to be set later + endpointId: SonicConstantsLib.LAYER_ZERO_V2_ENDPOINT_ID, + endpoint: SonicConstantsLib.LAYER_ZERO_V2_ENDPOINT, + sendLib: SonicConstantsLib.LAYER_ZERO_V2_SEND_ULN_302, + receiveLib: SonicConstantsLib.LAYER_ZERO_V2_RECEIVE_ULN_302, + platform: SonicConstantsLib.PLATFORM, + executor: SonicConstantsLib.LAYER_ZERO_V2_EXECUTOR, + xToken: SonicConstantsLib.TOKEN_XSTBL, + xTokenBridge: address(0), + delegator: delegator + }); + } + + function createConfigAvalanche( + Vm vm, + uint forkId, + address delegator + ) internal returns (BridgeTestLib.ChainConfig memory) { + vm.selectFork(forkId); + return BridgeTestLib.ChainConfig({ + fork: forkId, + multisig: IPlatform(AvalancheConstantsLib.PLATFORM).multisig(), + oapp: address(0), // to be set later + endpointId: AvalancheConstantsLib.LAYER_ZERO_V2_ENDPOINT_ID, + endpoint: AvalancheConstantsLib.LAYER_ZERO_V2_ENDPOINT, + sendLib: AvalancheConstantsLib.LAYER_ZERO_V2_SEND_ULN_302, + receiveLib: AvalancheConstantsLib.LAYER_ZERO_V2_RECEIVE_ULN_302, + platform: AvalancheConstantsLib.PLATFORM, + executor: AvalancheConstantsLib.LAYER_ZERO_V2_EXECUTOR, + xToken: address(0), + xTokenBridge: address(0), + delegator: delegator + }); + } + + function createConfigPlasma( + Vm vm, + uint forkId, + address delegator + ) internal returns (BridgeTestLib.ChainConfig memory) { + vm.selectFork(forkId); + return BridgeTestLib.ChainConfig({ + fork: forkId, + multisig: IPlatform(PlasmaConstantsLib.PLATFORM).multisig(), + oapp: address(0), // to be set later + endpointId: PlasmaConstantsLib.LAYER_ZERO_V2_ENDPOINT_ID, + endpoint: PlasmaConstantsLib.LAYER_ZERO_V2_ENDPOINT, + sendLib: PlasmaConstantsLib.LAYER_ZERO_V2_SEND_ULN_302, + receiveLib: PlasmaConstantsLib.LAYER_ZERO_V2_RECEIVE_ULN_302, + platform: PlasmaConstantsLib.PLATFORM, + executor: PlasmaConstantsLib.LAYER_ZERO_V2_EXECUTOR, + xToken: address(0), + xTokenBridge: address(0), + delegator: delegator + }); + } + + //endregion ------------------------------------- Chains + + //region ------------------------------------- Setup bridges + function setUpSonicAvalanche( + Vm vm, + BridgeTestLib.ChainConfig memory sonic, + BridgeTestLib.ChainConfig memory avalanche + ) internal { + // ------------------- Set up sending chain for Sonic:Plasma + vm.selectFork(sonic.fork); + vm.startPrank(sonic.delegator); + + { + address[] memory requiredDVNs = new address[](2); + requiredDVNs[0] = SonicConstantsLib.SONIC_DVN_LAYER_ZERO_PUSH; + requiredDVNs[1] = SonicConstantsLib.SONIC_DVN_HORIZEN_PUSH; + + _setupOAppOnChain( + sonic, + avalanche.endpointId, + requiredDVNs, + MIN_BLOCK_CONFIRMATIONS_SEND_SONIC, + MAX_MESSAGE_SIZE, + MIN_BLOCK_CONFIRMATIONS_RECEIVE_TARGET + ); + } + vm.stopPrank(); + + // ------------------- Set up sending chain for Avalanche:Plasma + vm.selectFork(avalanche.fork); + vm.startPrank(avalanche.delegator); + + { + address[] memory requiredDVNs = new address[](1); + requiredDVNs[0] = AvalancheConstantsLib.AVALANCHE_DVN_LAYER_ZERO_PUSH; + // requiredDVNs[1] = AVALANCHE_DVN_NETHERMIND_PULL; + // requiredDVNs[2] = AVALANCHE_DVN_HORIZON_PULL; + _setupOAppOnChain( + avalanche, + sonic.endpointId, + requiredDVNs, + MIN_BLOCK_CONFIRMATIONS_SEND_TARGET, + MAX_MESSAGE_SIZE, + MIN_BLOCK_CONFIRMATIONS_RECEIVE_TARGET + ); + } + + vm.stopPrank(); + + // ------------------- set peers + _setPeers(vm, sonic, avalanche); + } + + function setUpSonicPlasma( + Vm vm, + BridgeTestLib.ChainConfig memory sonic, + BridgeTestLib.ChainConfig memory plasma + ) internal { + // ------------------- Set up sending chain for Sonic:Plasma + vm.selectFork(sonic.fork); + vm.startPrank(sonic.delegator); + + { + address[] memory requiredDVNs = new address[](1); + requiredDVNs[0] = SonicConstantsLib.SONIC_DVN_LAYER_ZERO_PUSH; + + _setupOAppOnChain( + sonic, + plasma.endpointId, + requiredDVNs, + MIN_BLOCK_CONFIRMATIONS_SEND_SONIC, + MAX_MESSAGE_SIZE, + MIN_BLOCK_CONFIRMATIONS_RECEIVE_TARGET + ); + } + vm.stopPrank(); + + // ------------------- Set up receiving chain for Sonic:Plasma + vm.selectFork(plasma.fork); + vm.startPrank(plasma.delegator); + + { + address[] memory requiredDVNs = new address[](2); + requiredDVNs[0] = PlasmaConstantsLib.PLASMA_DVN_LAYER_ZERO_PUSH; + requiredDVNs[1] = PlasmaConstantsLib.PLASMA_DVN_NETHERMIND_PUSH; + // requiredDVNs[2] = PLASMA_DVN_HORIZON; + + _setupOAppOnChain( + plasma, + sonic.endpointId, + requiredDVNs, + MIN_BLOCK_CONFIRMATIONS_SEND_TARGET, + MAX_MESSAGE_SIZE, + MIN_BLOCK_CONFIRMATIONS_RECEIVE_TARGET + ); + } + vm.stopPrank(); + + // ------------------- set peers + _setPeers(vm, sonic, plasma); + } + + function setUpAvalanchePlasma( + Vm vm, + BridgeTestLib.ChainConfig memory avalanche, + BridgeTestLib.ChainConfig memory plasma + ) internal { + // ------------------- Set up sending chain for Avalanche:Plasma + vm.selectFork(avalanche.fork); + vm.startPrank(avalanche.delegator); + + { + address[] memory requiredDVNs = new address[](1); + requiredDVNs[0] = AvalancheConstantsLib.AVALANCHE_DVN_LAYER_ZERO_PUSH; + // requiredDVNs[1] = AVALANCHE_DVN_NETHERMIND_PULL; + // requiredDVNs[2] = AVALANCHE_DVN_HORIZON_PULL; + _setupOAppOnChain( + avalanche, + plasma.endpointId, + requiredDVNs, + MIN_BLOCK_CONFIRMATIONS_SEND_TARGET, + MAX_MESSAGE_SIZE, + MIN_BLOCK_CONFIRMATIONS_RECEIVE_TARGET + ); + } + vm.stopPrank(); + + // ------------------- Set up receiving chain for Avalanche:Plasma + vm.selectFork(plasma.fork); + vm.startPrank(plasma.delegator); + { + address[] memory requiredDVNs = new address[](1); + requiredDVNs[0] = PlasmaConstantsLib.PLASMA_DVN_LAYER_ZERO_PUSH; + _setupOAppOnChain( + plasma, + avalanche.endpointId, + requiredDVNs, + MIN_BLOCK_CONFIRMATIONS_SEND_TARGET, + MAX_MESSAGE_SIZE, + MIN_BLOCK_CONFIRMATIONS_RECEIVE_TARGET + ); + } + vm.stopPrank(); + + // ------------------- set peers + _setPeers(vm, avalanche, plasma); + } + + //endregion ------------------------------------- Setup bridges + + //region ------------------------------------- Delegator + function _setupOAppOnChain( + BridgeTestLib.ChainConfig memory src, + uint32 dstEndpointId, + address[] memory requiredDVNs, + uint64 confirmations, + uint32 maxMessageSize, + uint64 receiveConfirmations + ) internal { + // assume here that fork and msg.sender are already correct + bool bothWays = receiveConfirmations != 0; + + _setupLayerZeroConfig(src, dstEndpointId, bothWays); + _setSendConfig(src, dstEndpointId, requiredDVNs, confirmations, maxMessageSize); + if (bothWays) { + _setReceiveConfig(src, dstEndpointId, requiredDVNs, receiveConfirmations); + } + } + + //endregion ------------------------------------- Delegator + + //region ------------------------------------- Layer zero utils + function _setupLayerZeroConfig( + BridgeTestLib.ChainConfig memory src, + uint32 dstEndpointId, + bool setupBothWays + ) internal { + // assume that fork and msg.sender are already correct + + if (src.sendLib != address(0)) { + // Set send library for outbound messages + ILayerZeroEndpointV2(src.endpoint) + .setSendLibrary( + src.oapp, // OApp address + dstEndpointId, // Destination chain EID + src.sendLib // SendUln302 address + ); + } + + // Set receive library for inbound messages + if (setupBothWays) { + ILayerZeroEndpointV2(src.endpoint) + .setReceiveLibrary( + src.oapp, // OApp address + dstEndpointId, // Source chain EID + src.receiveLib, // ReceiveUln302 address + 0 // Grace period for library switch + ); + } + } + + function _setPeers(Vm vm, BridgeTestLib.ChainConfig memory src, BridgeTestLib.ChainConfig memory dst) internal { + // ------------------- Sonic: set up peer connection + vm.selectFork(src.fork); + + vm.prank(src.multisig); + IOAppCore(src.oapp).setPeer(dst.endpointId, bytes32(uint(uint160(address(dst.oapp))))); + + // ------------------- Avalanche: set up peer connection + vm.selectFork(dst.fork); + + vm.prank(dst.multisig); + IOAppCore(dst.oapp).setPeer(src.endpointId, bytes32(uint(uint160(address(src.oapp))))); + } + + /// @notice Configures both ULN (DVN validators) and Executor for an OApp + /// @param requiredDVNs Array of DVN validator addresses + /// @param confirmations Minimum block confirmations + function _setSendConfig( + BridgeTestLib.ChainConfig memory src, + uint32 dstEndpointId, + address[] memory requiredDVNs, + uint64 confirmations, + uint32 maxMessageSize + ) internal { + // assume that fork and msg.sender are already correct + + // ---------------------- ULN (DVN) configuration ---------------------- + UlnConfig memory uln = UlnConfig({ + confirmations: confirmations, + requiredDVNCount: uint8(requiredDVNs.length), + optionalDVNCount: type(uint8).max, + requiredDVNs: requiredDVNs, // sorted list of required DVN addresses + optionalDVNs: new address[](0), + optionalDVNThreshold: 0 + }); + + ExecutorConfig memory exec = ExecutorConfig({ + maxMessageSize: maxMessageSize, // max bytes per cross-chain message + executor: src.executor // address that pays destination execution fees + }); + + bytes memory encodedUln = abi.encode(uln); + bytes memory encodedExec = abi.encode(exec); + + SetConfigParam[] memory params = new SetConfigParam[](2); + params[0] = SetConfigParam({eid: dstEndpointId, configType: CONFIG_TYPE_EXECUTOR, config: encodedExec}); + params[1] = SetConfigParam({eid: dstEndpointId, configType: CONFIG_TYPE_ULN, config: encodedUln}); + + ILayerZeroEndpointV2(src.endpoint).setConfig(src.oapp, src.sendLib, params); + } + + /// @notice Configures ULN (DVN validators) for on receiving chain + /// @dev https://docs.layerzero.network/v2/developers/evm/configuration/dvn-executor-config + /// @param requiredDVNs Array of DVN validator addresses + /// @param confirmations Minimum block confirmations for ULN + function _setReceiveConfig( + BridgeTestLib.ChainConfig memory src, + uint32 dstEndpointId, + address[] memory requiredDVNs, + uint64 confirmations + ) internal { + // assume that fork and msg.sender are already correct + + // ---------------------- ULN (DVN) configuration ---------------------- + UlnConfig memory uln = UlnConfig({ + confirmations: confirmations, // Minimum block confirmations + requiredDVNCount: uint8(requiredDVNs.length), + optionalDVNCount: type(uint8).max, + requiredDVNs: requiredDVNs, // sorted list of required DVN addresses + optionalDVNs: new address[](0), + optionalDVNThreshold: 0 + }); + + SetConfigParam[] memory params = new SetConfigParam[](1); + params[0] = SetConfigParam({eid: dstEndpointId, configType: CONFIG_TYPE_ULN, config: abi.encode(uln)}); + + ILayerZeroEndpointV2(src.endpoint).setConfig(src.oapp, src.receiveLib, params); + } + + /// @notice Calls getConfig on the specified LayerZero Endpoint. + /// @dev Decodes the returned bytes as a UlnConfig. Logs some of its fields. + /// @dev https://docs.layerzero.network/v2/developers/evm/configuration/dvn-executor-config + /// @param endpoint_ The LayerZero Endpoint address. + /// @param oapp_ The address of your OApp. + /// @param lib_ The address of the Message Library (send or receive). + /// @param eid_ The remote endpoint identifier. + /// @param configType_ The configuration type (1 = Executor, 2 = ULN). + function _getConfig( + Vm vm, + uint forkId, + address endpoint_, + address oapp_, + address lib_, + uint32 eid_, + uint32 configType_ + ) internal { + // Create a fork from the specified RPC URL. + vm.selectFork(forkId); + vm.startBroadcast(); + + // Instantiate the LayerZero endpoint. + ILayerZeroEndpointV2 endpoint = ILayerZeroEndpointV2(endpoint_); + // Retrieve the raw configuration bytes. + bytes memory config = endpoint.getConfig(oapp_, lib_, eid_, configType_); + + if (configType_ == 1) { + // Decode the Executor config (configType = 1) + ExecutorConfig memory execConfig = abi.decode(config, (ExecutorConfig)); + // Log some key configuration parameters. + console.log("Executor maxMessageSize:", execConfig.maxMessageSize); + console.log("Executor Address:", execConfig.executor); + } + + if (configType_ == 2) { + // Decode the ULN config (configType = 2) + UlnConfig memory decodedConfig = abi.decode(config, (UlnConfig)); + // Log some key configuration parameters. + console.log("Confirmations:", decodedConfig.confirmations); + console.log("Required DVN Count:", decodedConfig.requiredDVNCount); + for (uint i = 0; i < decodedConfig.requiredDVNs.length; i++) { + console.logAddress(decodedConfig.requiredDVNs[i]); + } + console.log("Optional DVN Count:", decodedConfig.optionalDVNCount); + for (uint i = 0; i < decodedConfig.optionalDVNs.length; i++) { + console.logAddress(decodedConfig.optionalDVNs[i]); + } + console.log("Optional DVN Threshold:", decodedConfig.optionalDVNThreshold); + } + vm.stopBroadcast(); + } + + /// @notice Extract PacketSent message from emitted event + function _extractSendMessage(Vm.Log[] memory logs) internal pure returns (bytes memory message, bytes32 guid) { + bytes memory encodedPayload; + bytes32 sig = keccak256("PacketSent(bytes,bytes,address)"); // PacketSent(bytes encodedPayload, bytes options, address sendLibrary) + + for (uint i; i < logs.length; ++i) { + if (logs[i].topics[0] == sig) { + (encodedPayload,,) = abi.decode(logs[i].data, (bytes, bytes, address)); + break; + } + } + + // repeat decoding logic from Packet.sol\decode() and PacketV1Codec.sol\message() + { // message = bytes(encodedPayload[113:]); + + // header length: 1 + 8 + 4 + 32 + 4 + 32 + 32 = 113 + uint start = 113; + require(encodedPayload.length >= start, "encodedPayload too short"); + uint msgLen = encodedPayload.length - start; + message = new bytes(msgLen); + for (uint i = 0; i < msgLen; ++i) { + message[i] = encodedPayload[start + i]; + } + } + + assembly { + guid := mload(add(encodedPayload, add(32, 81))) + } + } + + function _extractPayload(Vm.Log[] memory logs) internal pure returns (bytes memory encodedPayload) { + bytes32 sig = keccak256("PacketSent(bytes,bytes,address)"); // PacketSent(bytes encodedPayload, bytes options, address sendLibrary) + + for (uint i; i < logs.length; ++i) { + if (logs[i].topics[0] == sig) { + (encodedPayload,,) = abi.decode(logs[i].data, (bytes, bytes, address)); + break; + } + } + + return encodedPayload; + } + + /// @notice Extract ComposeSent message from emitted event + function _extractComposeMessage(Vm + .Log[] memory logs) internal pure returns (address from, address to, bytes memory message) { + bytes32 sig = keccak256("ComposeSent(address,address,bytes32,uint16,bytes)"); // ComposeSent(address from, address to, bytes32 guid, uint16 index, bytes message) + + for (uint i; i < logs.length; ++i) { + if (logs[i].topics[0] == sig) { + (from, to,,, message) = abi.decode(logs[i].data, (address, address, bytes32, uint16, bytes)); + break; + } + } + + // console.logBytes(message); + return (from, to, message); + } + + /// @notice Extract XTokenSent message from emitted event + function _extractXTokenSentMessage(Vm + .Log[] memory logs) + internal + pure + returns ( + address userFrom, + uint32 dstEid, + uint amount, + uint amountSentLD, + bytes32 guidId, + uint64 nonce, + uint nativeFee + ) + { + // event XTokenSent(address indexed userFrom, uint32 indexed dstEid, uint amount, uint amountSentLD, bytes32 indexed guidId, uint64 nonce, uint nativeFee); + bytes32 sig = keccak256("XTokenSent(address,uint32,uint256,uint256,bytes32,uint64,uint256)"); + + for (uint i; i < logs.length; ++i) { + if (logs[i].topics[0] != sig) continue; + + // extract indexed out of topics + // topics = [sig, userFrom, dstEid, guidId] + require(logs[i].topics.length >= 4, "not enough topics for indexed params"); + userFrom = address(uint160(uint(logs[i].topics[1]))); + dstEid = uint32(uint(logs[i].topics[2])); + guidId = bytes32(logs[i].topics[3]); + + // extract all other params from data: amount, amountSentLD, nonce, nativeFee + require(logs[i].data.length >= 32 * 4, "data too short for non-indexed params"); + (amount, amountSentLD, nonce, nativeFee) = abi.decode(logs[i].data, (uint, uint, uint64, uint)); + break; + } + + return (userFrom, dstEid, amount, amountSentLD, guidId, nonce, nativeFee); + } + + //endregion ------------------------------------- Layer zero utils + + /// @notice Empty function to exclude this test from coverage + function test() public {} +}