diff --git a/.gitignore b/.gitignore index fc12a58e..ba6f2516 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,9 @@ bootstrap.out # MacOS .DS_Store + +# Fuzz +crytic-export +echidna-corpus +medusa-corpus +med-logs \ No newline at end of file diff --git a/echidna-config.yaml b/echidna-config.yaml new file mode 100644 index 00000000..5ad13e76 --- /dev/null +++ b/echidna-config.yaml @@ -0,0 +1,54 @@ +workers: 1 + +testMode: assertion + +prefix: echidna_ +corpusDir: echidna-corpus + +testLimit: 100000000000 +# testLimit: 1000000 +codeSize: 100000 + +shrinkLimit: 1000 + +seqLen: 100 + +deployer: "0xfffff" +sender: ["0x10000", "0x20000", "0x30000"] + +filterBlacklist: true +filterFunctions: + [ + "Fuzz.handler_activateWithdrawalQueue()", + "Fuzz.handler_deactivateWithdrawalQueue()", + "Fuzz.handler_deposit(uint8,uint256,uint256)", + "Fuzz.handler_depositETH(uint256,uint256)", + "Fuzz.handler_depositTo(uint8,uint8,uint256,uint256)", + "Fuzz.handler_depositToETH(uint8,uint256,uint256)", + "Fuzz.handler_finaliseQueuedWithdrawal(uint8,uint256)", + "Fuzz.handler_finaliseQueuedWithdrawalsAggregated(uint8,uint8,uint256,uint256)", + "Fuzz.handler_onMessageReceiveChild(bool,uint8,uint8,uint8,uint256)", + "Fuzz.handler_onMessageReceiveRoot(bool,uint8,uint8,uint8,uint256)", + "Fuzz.handler_pauseChild()", + "Fuzz.handler_pauseRoot()", + "Fuzz.handler_setRateControlThreshold(uint8,uint256,uint256,uint256)", + "Fuzz.handler_setWithdrawalDelay(uint256)", + "Fuzz.handler_unpauseChild()", + "Fuzz.handler_unpauseRoot()", + "Fuzz.handler_updateImxCumulativeDepositLimit(uint256)", + "Fuzz.handler_withdraw(uint8,uint256,uint256)", + "Fuzz.handler_withdrawETH(uint256,uint256)", + "Fuzz.handler_withdrawETHTo(uint8,uint256,uint256)", + "Fuzz.handler_withdrawIMX(uint256,uint256)", + "Fuzz.handler_withdrawIMXTo(uint8,uint256,uint256)", + "Fuzz.handler_withdrawTo(uint8,uint8,uint256,uint256)", + "Fuzz.handler_withdrawWIMX(uint256,uint256)", + "Fuzz.handler_withdrawWIMXTo(uint8,uint256,uint256)", + ] + +cryticArgs: ["--foundry-compile-all"] + +# Initial Ether balance of contractAddr +balanceContract: 0xffffffffffffffffffffffffffffffffffffffffffffffff +# maximum value to send to payable functions +maxValue: 100000000000000000000000000000 # 100000000000 eth diff --git a/medusa.json b/medusa.json new file mode 100644 index 00000000..f01a9b53 --- /dev/null +++ b/medusa.json @@ -0,0 +1,116 @@ +{ + "fuzzing": { + "workers": 1, + "workerResetLimit": 50, + "timeout": 0, + "testLimit": 1000000, + "callSequenceLength": 100, + "corpusDirectory": "medusa-corpus", + "coverageEnabled": true, + "deploymentOrder": [ + "Fuzz" + ], + "targetContracts": [ + "Fuzz" + ], + "targetContractsBalances": [ + "0xffffffffffffffffffffffffffffffffffffffffffffffff" + ], + "constructorArgs": {}, + "deployerAddress": "0xfffff", + "senderAddresses": [ + "0x10000", + "0x20000", + "0x30000" + ], + "blockNumberDelayMax": 60480, + "blockTimestampDelayMax": 604800, + "blockGasLimit": 125000000, + "transactionGasLimit": 12500000, + "testing": { + "stopOnFailedTest": false, + "stopOnFailedContractMatching": true, + "stopOnNoTests": true, + "testAllContracts": false, + "traceAll": false, + "excludeFunctionSignatures": [ + "Fuzz.handler_activateWithdrawalQueue()", + "Fuzz.handler_deactivateWithdrawalQueue()", + "Fuzz.handler_deposit(uint8,uint256,uint256)", + "Fuzz.handler_depositETH(uint256,uint256)", + "Fuzz.handler_depositTo(uint8,uint8,uint256,uint256)", + "Fuzz.handler_depositToETH(uint8,uint256,uint256)", + "Fuzz.handler_finaliseQueuedWithdrawal(uint8,uint256)", + "Fuzz.handler_finaliseQueuedWithdrawalsAggregated(uint8,uint8,uint256,uint256)", + "Fuzz.handler_onMessageReceiveChild(bool,uint8,uint8,uint8,uint256)", + "Fuzz.handler_onMessageReceiveRoot(bool,uint8,uint8,uint8,uint256)", + "Fuzz.handler_pauseChild()", + "Fuzz.handler_pauseRoot()", + "Fuzz.handler_setRateControlThreshold(uint8,uint256,uint256,uint256)", + "Fuzz.handler_setWithdrawalDelay(uint256)", + "Fuzz.handler_unpauseChild()", + "Fuzz.handler_unpauseRoot()", + "Fuzz.handler_updateImxCumulativeDepositLimit(uint256)", + "Fuzz.handler_withdraw(uint8,uint256,uint256)", + "Fuzz.handler_withdrawETH(uint256,uint256)", + "Fuzz.handler_withdrawETHTo(uint8,uint256,uint256)", + "Fuzz.handler_withdrawIMX(uint256,uint256)", + "Fuzz.handler_withdrawIMXTo(uint8,uint256,uint256)", + "Fuzz.handler_withdrawTo(uint8,uint8,uint256,uint256)", + "Fuzz.handler_withdrawWIMX(uint256,uint256)", + "Fuzz.handler_withdrawWIMXTo(uint8,uint256,uint256)" + ], + "assertionTesting": { + "enabled": true, + "testViewMethods": true, + "panicCodeConfig": { + "failOnCompilerInsertedPanic": false, + "failOnAssertion": true, + "failOnArithmeticUnderflow": false, + "failOnDivideByZero": false, + "failOnEnumTypeConversionOutOfBounds": false, + "failOnIncorrectStorageAccess": false, + "failOnPopEmptyArray": false, + "failOnOutOfBoundsArrayAccess": false, + "failOnAllocateTooMuchMemory": false, + "failOnCallUninitializedVariable": false + } + }, + "propertyTesting": { + "enabled": false, + "testPrefixes": [ + "property_" + ] + }, + "optimizationTesting": { + "enabled": false, + "testPrefixes": [ + "optimize_" + ] + } + }, + "chainConfig": { + "codeSizeCheckDisabled": true, + "cheatCodes": { + "cheatCodesEnabled": true, + "enableFFI": false + } + } + }, + "compilation": { + "platform": "crytic-compile", + "platformConfig": { + "target": ".", + "solcVersion": "", + "exportDirectory": "", + "args": [ + "--foundry-compile-all" + ] + } + }, + "logging": { + "level": "info", + "logDirectory": "med-logs", + "noColor": true + } +} \ No newline at end of file diff --git a/package.json b/package.json index 26abae6a..3e433de0 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "@nomicfoundation/hardhat-toolbox": "^4.0.0", "@nomicfoundation/hardhat-verify": "^2.0.0", "@nomiclabs/hardhat-ethers": "npm:hardhat-deploy-ethers", + "@perimetersec/fuzzlib": "0.3.0", "@typechain/ethers-v6": "^0.5.0", "@typechain/hardhat": "^9.0.0", "@types/chai": "^4.2.0", diff --git a/remappings.txt b/remappings.txt index 7e76cacb..b3181269 100644 --- a/remappings.txt +++ b/remappings.txt @@ -2,4 +2,5 @@ @openzeppelin/contracts-upgradeable=lib/openzeppelin-contracts-upgradeable/contracts @axelar-cgp-solidity=lib/axelar-cgp-solidity @axelar-gmp-sdk-solidity=lib/axelar-gmp-sdk-solidity -@axelar-network/axelar-gmp-sdk-solidity/=lib/axelar-gmp-sdk-solidity/ \ No newline at end of file +@axelar-network/axelar-gmp-sdk-solidity/=lib/axelar-gmp-sdk-solidity/ +@perimetersec/fuzzlib/src/=node_modules/@perimetersec/fuzzlib/src/ \ No newline at end of file diff --git a/test/fuzzing/Fuzz.sol b/test/fuzzing/Fuzz.sol new file mode 100644 index 00000000..eec9dab1 --- /dev/null +++ b/test/fuzzing/Fuzz.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.0; + +import "./FuzzIntegrityChildERC20Bridge.sol"; +import "./FuzzIntegrityRootERC20BridgeFlowRate.sol"; + +/** + * @title Fuzz + * @author 0xScourgedev + * @notice Composite contract for all of the handlers + */ +contract Fuzz is FuzzChildERC20BridgeIntegrity, FuzzRootERC20BridgeFlowRateIntegrity { + constructor() payable { + setup(); + setupActors(); + } +} diff --git a/test/fuzzing/FuzzIntegrityBase.sol b/test/fuzzing/FuzzIntegrityBase.sol new file mode 100644 index 00000000..7d892271 --- /dev/null +++ b/test/fuzzing/FuzzIntegrityBase.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.0; + +/** + * @title FuzzIntegrityBase + * @author 0xScourgedev + * @notice Contains the base for all fuzz integrity contracts + */ +abstract contract FuzzIntegrityBase { + /** + * @notice Executes a delegatecall to the this contract with the given callData + * @dev This function is used to call the handlers in order to test the integrity of the handlers + * @param callData The data to be used in the delegatecall + * @return the success of the delegatecall and the return data + */ + function _testSelf(bytes memory callData) internal returns (bool, bytes4) { + (bool success, bytes memory returnData) = address(this).delegatecall(callData); + + return (success, bytes4(returnData)); + } +} diff --git a/test/fuzzing/FuzzIntegrityChildERC20Bridge.sol b/test/fuzzing/FuzzIntegrityChildERC20Bridge.sol new file mode 100644 index 00000000..0366ff01 --- /dev/null +++ b/test/fuzzing/FuzzIntegrityChildERC20Bridge.sol @@ -0,0 +1,195 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.0; + +import "./helper/handlers/HandlerChildERC20Bridge.sol"; +import "./FuzzIntegrityBase.sol"; + +/** + * @title FuzzChildERC20BridgeIntegrity + * @author 0xScourgedev + * @notice Checks for errors in the handlers for ChildERC20Bridge + */ +contract FuzzChildERC20BridgeIntegrity is HandlerChildERC20Bridge, FuzzIntegrityBase { + /////////////////////////////////////////////////////////////////////////////////////////////// + // INTEGRITY // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * @notice Checks the integrity of handler_onMessageReceiveChild + */ + function fuzz_onMessageReceiveChild( + bool isDeposit, + uint8 rootTokenSelector, + uint8 senderSelector, + uint8 receiverSelector, + uint256 amount + ) public { + bytes memory callData = abi.encodeWithSelector( + HandlerChildERC20Bridge.handler_onMessageReceiveChild.selector, + isDeposit, + rootTokenSelector, + senderSelector, + receiverSelector, + amount + ); + + (bool success, bytes4 errorSelector) = _testSelf(callData); + if (!success) { + bytes4[] memory allowedErrors = new bytes4[](1); + allowedErrors[0] = ClampFail.selector; + fl.errAllow(errorSelector, allowedErrors, "SELF-01"); + } + } + + /** + * @notice Checks the integrity of handler_pauseChild + */ + function fuzz_pauseChild() public { + bytes memory callData = abi.encodeWithSelector(HandlerChildERC20Bridge.handler_pauseChild.selector); + + (bool success, bytes4 errorSelector) = _testSelf(callData); + if (!success) { + bytes4[] memory allowedErrors = new bytes4[](1); + allowedErrors[0] = ClampFail.selector; + fl.errAllow(errorSelector, allowedErrors, "SELF-02"); + } + } + + /** + * @notice Checks the integrity of handler_unpauseChild + */ + function fuzz_unpauseChild() public { + bytes memory callData = abi.encodeWithSelector(HandlerChildERC20Bridge.handler_unpauseChild.selector); + + (bool success, bytes4 errorSelector) = _testSelf(callData); + if (!success) { + bytes4[] memory allowedErrors = new bytes4[](1); + allowedErrors[0] = ClampFail.selector; + fl.errAllow(errorSelector, allowedErrors, "SELF-03"); + } + } + + /** + * @notice Checks the integrity of handler_withdraw + */ + function fuzz_withdraw(uint8 childTokenSelector, uint256 amount, uint256 value) public { + bytes memory callData = + abi.encodeWithSelector(HandlerChildERC20Bridge.handler_withdraw.selector, childTokenSelector, amount, value); + + (bool success, bytes4 errorSelector) = _testSelf(callData); + if (!success) { + bytes4[] memory allowedErrors = new bytes4[](1); + allowedErrors[0] = ClampFail.selector; + fl.errAllow(errorSelector, allowedErrors, "SELF-04"); + } + } + + /** + * @notice Checks the integrity of handler_withdrawETH + */ + function fuzz_withdrawETH(uint256 amount, uint256 value) public { + bytes memory callData = + abi.encodeWithSelector(HandlerChildERC20Bridge.handler_withdrawETH.selector, amount, value); + + (bool success, bytes4 errorSelector) = _testSelf(callData); + if (!success) { + bytes4[] memory allowedErrors = new bytes4[](1); + allowedErrors[0] = ClampFail.selector; + fl.errAllow(errorSelector, allowedErrors, "SELF-05"); + } + } + + /** + * @notice Checks the integrity of handler_withdrawETHTo + */ + function fuzz_withdrawETHTo(uint8 receiverSelector, uint256 amount, uint256 value) public { + bytes memory callData = abi.encodeWithSelector( + HandlerChildERC20Bridge.handler_withdrawETHTo.selector, receiverSelector, amount, value + ); + + (bool success, bytes4 errorSelector) = _testSelf(callData); + if (!success) { + bytes4[] memory allowedErrors = new bytes4[](1); + allowedErrors[0] = ClampFail.selector; + fl.errAllow(errorSelector, allowedErrors, "SELF-06"); + } + } + + /** + * @notice Checks the integrity of handler_withdrawIMX + */ + function fuzz_withdrawIMX(uint256 amount, uint256 value) public { + bytes memory callData = + abi.encodeWithSelector(HandlerChildERC20Bridge.handler_withdrawIMX.selector, amount, value); + + (bool success, bytes4 errorSelector) = _testSelf(callData); + if (!success) { + bytes4[] memory allowedErrors = new bytes4[](1); + allowedErrors[0] = ClampFail.selector; + fl.errAllow(errorSelector, allowedErrors, "SELF-07"); + } + } + + /** + * @notice Checks the integrity of handler_withdrawIMXTo + */ + function fuzz_withdrawIMXTo(uint8 receiverSelector, uint256 amount, uint256 value) public { + bytes memory callData = abi.encodeWithSelector( + HandlerChildERC20Bridge.handler_withdrawIMXTo.selector, receiverSelector, amount, value + ); + + (bool success, bytes4 errorSelector) = _testSelf(callData); + if (!success) { + bytes4[] memory allowedErrors = new bytes4[](1); + allowedErrors[0] = ClampFail.selector; + fl.errAllow(errorSelector, allowedErrors, "SELF-08"); + } + } + + /** + * @notice Checks the integrity of handler_withdrawTo + */ + function fuzz_withdrawTo(uint8 childTokenSelector, uint8 receiverSelector, uint256 amount, uint256 value) public { + bytes memory callData = abi.encodeWithSelector( + HandlerChildERC20Bridge.handler_withdrawTo.selector, childTokenSelector, receiverSelector, amount, value + ); + + (bool success, bytes4 errorSelector) = _testSelf(callData); + if (!success) { + bytes4[] memory allowedErrors = new bytes4[](1); + allowedErrors[0] = ClampFail.selector; + fl.errAllow(errorSelector, allowedErrors, "SELF-09"); + } + } + + /** + * @notice Checks the integrity of handler_withdrawWIMX + */ + function fuzz_withdrawWIMX(uint256 amount, uint256 value) public { + bytes memory callData = + abi.encodeWithSelector(HandlerChildERC20Bridge.handler_withdrawWIMX.selector, amount, value); + + (bool success, bytes4 errorSelector) = _testSelf(callData); + if (!success) { + bytes4[] memory allowedErrors = new bytes4[](1); + allowedErrors[0] = ClampFail.selector; + fl.errAllow(errorSelector, allowedErrors, "SELF-10"); + } + } + + /** + * @notice Checks the integrity of handler_withdrawWIMXTo + */ + function fuzz_withdrawWIMXTo(uint8 receiverSelector, uint256 amount, uint256 value) public { + bytes memory callData = abi.encodeWithSelector( + HandlerChildERC20Bridge.handler_withdrawWIMXTo.selector, receiverSelector, amount, value + ); + + (bool success, bytes4 errorSelector) = _testSelf(callData); + if (!success) { + bytes4[] memory allowedErrors = new bytes4[](1); + allowedErrors[0] = ClampFail.selector; + fl.errAllow(errorSelector, allowedErrors, "SELF-11"); + } + } +} diff --git a/test/fuzzing/FuzzIntegrityRootERC20BridgeFlowRate.sol b/test/fuzzing/FuzzIntegrityRootERC20BridgeFlowRate.sol new file mode 100644 index 00000000..fe5d2259 --- /dev/null +++ b/test/fuzzing/FuzzIntegrityRootERC20BridgeFlowRate.sol @@ -0,0 +1,266 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.0; + +import "./helper/handlers/HandlerRootERC20BridgeFlowRate.sol"; +import "./FuzzIntegrityBase.sol"; + +/** + * @title FuzzRootERC20BridgeFlowRateIntegrity + * @author 0xScourgedev + * @notice Checks for errors in the handlers for RootERC20BridgeFlowRate + */ +contract FuzzRootERC20BridgeFlowRateIntegrity is HandlerRootERC20BridgeFlowRate, FuzzIntegrityBase { + /////////////////////////////////////////////////////////////////////////////////////////////// + // INTEGRITY // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * @notice Checks the integrity of handler_activateWithdrawalQueue + */ + function fuzz_activateWithdrawalQueue() public { + bytes memory callData = + abi.encodeWithSelector(HandlerRootERC20BridgeFlowRate.handler_activateWithdrawalQueue.selector); + + (bool success, bytes4 errorSelector) = _testSelf(callData); + if (!success) { + bytes4[] memory allowedErrors = new bytes4[](1); + allowedErrors[0] = ClampFail.selector; + fl.errAllow(errorSelector, allowedErrors, "SELF-12"); + } + } + + /** + * @notice Checks the integrity of handler_deactivateWithdrawalQueue + */ + function fuzz_deactivateWithdrawalQueue() public { + bytes memory callData = + abi.encodeWithSelector(HandlerRootERC20BridgeFlowRate.handler_deactivateWithdrawalQueue.selector); + + (bool success, bytes4 errorSelector) = _testSelf(callData); + if (!success) { + bytes4[] memory allowedErrors = new bytes4[](1); + allowedErrors[0] = ClampFail.selector; + fl.errAllow(errorSelector, allowedErrors, "SELF-13"); + } + } + + /** + * @notice Checks the integrity of handler_deposit + */ + function fuzz_deposit(uint8 rootTokenSelector, uint256 amount, uint256 value) public { + bytes memory callData = abi.encodeWithSelector( + HandlerRootERC20BridgeFlowRate.handler_deposit.selector, rootTokenSelector, amount, value + ); + + (bool success, bytes4 errorSelector) = _testSelf(callData); + if (!success) { + bytes4[] memory allowedErrors = new bytes4[](1); + allowedErrors[0] = ClampFail.selector; + fl.errAllow(errorSelector, allowedErrors, "SELF-14"); + } + } + + /** + * @notice Checks the integrity of handler_depositETH + */ + function fuzz_depositETH(uint256 amount, uint256 value) public { + bytes memory callData = + abi.encodeWithSelector(HandlerRootERC20BridgeFlowRate.handler_depositETH.selector, amount, value); + + (bool success, bytes4 errorSelector) = _testSelf(callData); + if (!success) { + bytes4[] memory allowedErrors = new bytes4[](1); + allowedErrors[0] = ClampFail.selector; + fl.errAllow(errorSelector, allowedErrors, "SELF-15"); + } + } + + /** + * @notice Checks the integrity of handler_depositTo + */ + function fuzz_depositTo(uint8 rootTokenSelector, uint8 receiverSelector, uint256 amount, uint256 value) public { + bytes memory callData = abi.encodeWithSelector( + HandlerRootERC20BridgeFlowRate.handler_depositTo.selector, + rootTokenSelector, + receiverSelector, + amount, + value + ); + + (bool success, bytes4 errorSelector) = _testSelf(callData); + if (!success) { + bytes4[] memory allowedErrors = new bytes4[](1); + allowedErrors[0] = ClampFail.selector; + fl.errAllow(errorSelector, allowedErrors, "SELF-16"); + } + } + + /** + * @notice Checks the integrity of handler_depositToETH + */ + function fuzz_depositToETH(uint8 receiver, uint256 amount, uint256 value) public { + bytes memory callData = abi.encodeWithSelector( + HandlerRootERC20BridgeFlowRate.handler_depositToETH.selector, receiver, amount, value + ); + + (bool success, bytes4 errorSelector) = _testSelf(callData); + if (!success) { + bytes4[] memory allowedErrors = new bytes4[](1); + allowedErrors[0] = ClampFail.selector; + fl.errAllow(errorSelector, allowedErrors, "SELF-17"); + } + } + + /** + * @notice Checks the integrity of handler_finaliseQueuedWithdrawal + */ + function fuzz_finaliseQueuedWithdrawal(uint8 receiverSelector, uint256 index) public { + bytes memory callData = abi.encodeWithSelector( + HandlerRootERC20BridgeFlowRate.handler_finaliseQueuedWithdrawal.selector, receiverSelector, index + ); + + (bool success, bytes4 errorSelector) = _testSelf(callData); + if (!success) { + bytes4[] memory allowedErrors = new bytes4[](1); + allowedErrors[0] = ClampFail.selector; + fl.errAllow(errorSelector, allowedErrors, "SELF-18"); + } + } + + /** + * @notice Checks the integrity of handler_finaliseQueuedWithdrawalsAggregated + */ + function fuzz_finaliseQueuedWithdrawalsAggregated( + uint8 receiverSelector, + uint8 tokenSelector, + uint256 length, + uint256 entropy + ) public { + bytes memory callData = abi.encodeWithSelector( + HandlerRootERC20BridgeFlowRate.handler_finaliseQueuedWithdrawalsAggregated.selector, + receiverSelector, + tokenSelector, + length, + entropy + ); + + (bool success, bytes4 errorSelector) = _testSelf(callData); + if (!success) { + bytes4[] memory allowedErrors = new bytes4[](1); + allowedErrors[0] = ClampFail.selector; + fl.errAllow(errorSelector, allowedErrors, "SELF-19"); + } + } + + /** + * @notice Checks the integrity of handler_onMessageReceiveRoot + */ + function fuzz_onMessageReceiveRoot( + bool isWithdraw, + uint8 rootTokenSelector, + uint8 withdrawerSelector, + uint8 receiverSelector, + uint256 amount + ) public { + bytes memory callData = abi.encodeWithSelector( + HandlerRootERC20BridgeFlowRate.handler_onMessageReceiveRoot.selector, + isWithdraw, + rootTokenSelector, + withdrawerSelector, + receiverSelector, + amount + ); + + (bool success, bytes4 errorSelector) = _testSelf(callData); + if (!success) { + bytes4[] memory allowedErrors = new bytes4[](1); + allowedErrors[0] = ClampFail.selector; + fl.errAllow(errorSelector, allowedErrors, "SELF-20"); + } + } + + /** + * @notice Checks the integrity of handler_pauseRoot + */ + function fuzz_pauseRoot() public { + bytes memory callData = abi.encodeWithSelector(HandlerRootERC20BridgeFlowRate.handler_pauseRoot.selector); + + (bool success, bytes4 errorSelector) = _testSelf(callData); + if (!success) { + bytes4[] memory allowedErrors = new bytes4[](1); + allowedErrors[0] = ClampFail.selector; + fl.errAllow(errorSelector, allowedErrors, "SELF-21"); + } + } + + /** + * @notice Checks the integrity of handler_setRateControlThreshold + */ + function fuzz_setRateControlThreshold( + uint8 tokenSelector, + uint256 capacity, + uint256 refillRate, + uint256 largeTransferThreshold + ) public { + bytes memory callData = abi.encodeWithSelector( + HandlerRootERC20BridgeFlowRate.handler_setRateControlThreshold.selector, + tokenSelector, + capacity, + refillRate, + largeTransferThreshold + ); + + (bool success, bytes4 errorSelector) = _testSelf(callData); + if (!success) { + bytes4[] memory allowedErrors = new bytes4[](1); + allowedErrors[0] = ClampFail.selector; + fl.errAllow(errorSelector, allowedErrors, "SELF-22"); + } + } + + /** + * @notice Checks the integrity of handler_setWithdrawalDelay + */ + function fuzz_setWithdrawalDelay(uint256 delay) public { + bytes memory callData = + abi.encodeWithSelector(HandlerRootERC20BridgeFlowRate.handler_setWithdrawalDelay.selector, delay); + + (bool success, bytes4 errorSelector) = _testSelf(callData); + if (!success) { + bytes4[] memory allowedErrors = new bytes4[](1); + allowedErrors[0] = ClampFail.selector; + fl.errAllow(errorSelector, allowedErrors, "SELF-23"); + } + } + + /** + * @notice Checks the integrity of handler_unpauseRoot + */ + function fuzz_unpauseRoot() public { + bytes memory callData = abi.encodeWithSelector(HandlerRootERC20BridgeFlowRate.handler_unpauseRoot.selector); + + (bool success, bytes4 errorSelector) = _testSelf(callData); + if (!success) { + bytes4[] memory allowedErrors = new bytes4[](1); + allowedErrors[0] = ClampFail.selector; + fl.errAllow(errorSelector, allowedErrors, "SELF-24"); + } + } + + /** + * @notice Checks the integrity of handler_updateImxCumulativeDepositLimit + */ + function fuzz_updateImxCumulativeDepositLimit(uint256 newImxCumulativeDepositLimit) public { + bytes memory callData = abi.encodeWithSelector( + HandlerRootERC20BridgeFlowRate.handler_updateImxCumulativeDepositLimit.selector, + newImxCumulativeDepositLimit + ); + + (bool success, bytes4 errorSelector) = _testSelf(callData); + if (!success) { + bytes4[] memory allowedErrors = new bytes4[](1); + allowedErrors[0] = ClampFail.selector; + fl.errAllow(errorSelector, allowedErrors, "SELF-25"); + } + } +} diff --git a/test/fuzzing/FuzzSetup.sol b/test/fuzzing/FuzzSetup.sol new file mode 100644 index 00000000..0a992a5e --- /dev/null +++ b/test/fuzzing/FuzzSetup.sol @@ -0,0 +1,254 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.0; + +import "@perimetersec/fuzzlib/src/FuzzBase.sol"; + +import "./helper/FuzzStorageVariables.sol"; + +/** + * @title FuzzSetup + * @author 0xScourgedev + * @notice Setup for the fuzzing suite + */ +contract FuzzSetup is FuzzBase, FuzzStorageVariables { + /////////////////////////////////////////////////////////////////////////////////////////////// + // SETUP ACTORS // + /////////////////////////////////////////////////////////////////////////////////////////////// + function setupActors() internal { + mintAndApproveTokens(); + } + + function mintAndApproveTokens() private { + // First send the wrapped native tokens to the harness + WETH(payable(wETH)).deposit{value: INITIAL_WETH_BALANCE * USERS.length}(); + WIMX(payable(wIMX)).deposit{value: INITIAL_WETH_BALANCE * USERS.length}(); + + for (uint256 i = 0; i < USERS.length; i++) { + for (uint256 j = 0; j < rootTokens.length; j++) { + // Approvals for rootTokens for all targets + for (uint256 k = 0; k < targets.length; k++) { + vm.prank(USERS[i]); + ChildERC20(rootTokens[j]).approve(targets[k], type(uint128).max); + } + + // WETH is not minted here, so skip it + if (rootTokens[j] == wETH) { + continue; + } + // Mint root tokens to the users + ChildERC20(rootTokens[j]).mint(USERS[i], MAX_ERC20_BALANCE * 10 ** ChildERC20(rootTokens[j]).decimals()); + // Check that the minting was successful + assert( + ChildERC20(rootTokens[j]).balanceOf(USERS[i]) + == MAX_ERC20_BALANCE * 10 ** ChildERC20(rootTokens[j]).decimals() + ); + } + + // Can't mint child tokens, since only the bridge can, so just approve them + for (uint256 j = 0; j < childTokens.length; j++) { + // Approvals for childTokens for all targets + for (uint256 k = 0; k < targets.length; k++) { + vm.prank(USERS[i]); + ChildERC20(childTokens[j]).approve(targets[k], type(uint128).max); + } + } + + // Send initial balance of native tokens to the users + (bool success,) = (USERS[i]).call{value: INITIAL_BALANCE}(""); + assert(success); + assert(USERS[i].balance == INITIAL_BALANCE); + + // Transfer wrapped native tokens to the users + WETH(payable(wETH)).transfer(USERS[i], INITIAL_WETH_BALANCE); + assert(WETH(payable(wETH)).balanceOf(USERS[i]) == INITIAL_WETH_BALANCE); + + WIMX(payable(wIMX)).transfer(USERS[i], INITIAL_WETH_BALANCE); + assert(WIMX(payable(wIMX)).balanceOf(USERS[i]) == INITIAL_WETH_BALANCE); + + assert(WIMX(payable(wIMX)).allowance(USERS[i], childERC20Bridge) == type(uint128).max); + } + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // SETUP CONTRACTS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function setup() internal { + createTokenTemplate(); + createMockAdaptors(); + createRootIMXToken(); + createWrappedNatives(); + createChildBridge(); + createRootBridge(); + initializeMockAdaptors(); + createInitialTokens(); + generateMapTokens(); + addTokensToArrays(); + addAddressesToTargets(); + } + + function createTokenTemplate() private { + tokenTemplate = address(new ChildERC20()); + ChildERC20(tokenTemplate).initialize(address(123), "Test", "TST", 18); + } + + function createMockAdaptors() private { + mockAdaptorRoot = address(new MockAdaptor()); + mockAdaptorChild = address(new MockAdaptor()); + } + + function createRootIMXToken() private { + rootIMXToken = address(new ChildERC20()); + ChildERC20(rootIMXToken).initialize(address(123), "Immutable X", "IMX", 18); + } + + function createWrappedNatives() private { + wETH = address(new WETH()); + wIMX = address(new WIMX()); + } + + function createChildBridge() private { + childERC20Bridge = address(new ChildERC20Bridge(address(this))); + IChildERC20Bridge.InitializationRoles memory childRoles = IChildERC20Bridge.InitializationRoles({ + defaultAdmin: address(this), + pauser: address(this), + unpauser: address(this), + adaptorManager: address(this), + initialDepositor: address(this), + treasuryManager: address(this) + }); + ChildERC20Bridge(payable(childERC20Bridge)).initialize( + childRoles, mockAdaptorChild, tokenTemplate, rootIMXToken, wIMX + ); + } + + function createRootBridge() private { + rootERC20BridgeFlowRate = address(new RootERC20BridgeFlowRate(address(this))); + IRootERC20Bridge.InitializationRoles memory rootRoles = IRootERC20Bridge.InitializationRoles({ + defaultAdmin: address(this), + pauser: address(this), + unpauser: address(this), + variableManager: address(this), + adaptorManager: address(this) + }); + RootERC20BridgeFlowRate(payable(rootERC20BridgeFlowRate)).initialize( + rootRoles, + mockAdaptorRoot, + childERC20Bridge, + tokenTemplate, + address(rootIMXToken), + address(wETH), + IMX_DEPOSIT_LIMIT, + address(this) + ); + } + + function initializeMockAdaptors() private { + MockAdaptor(mockAdaptorRoot).initialize(rootERC20BridgeFlowRate); + MockAdaptor(mockAdaptorChild).initialize(childERC20Bridge); + } + + function createInitialTokens() private { + rootSixDecimalInitial = address(new ChildERC20()); + ChildERC20(rootSixDecimalInitial).initialize(address(123), "Six Root Initial", "6RI", 6); + + rootEightDecimalInitial = address(new ChildERC20()); + ChildERC20(rootEightDecimalInitial).initialize(address(123), "Eight Root Initial", "8RI", 8); + + rootEighteenDecimalInitial = address(new ChildERC20()); + ChildERC20(rootEighteenDecimalInitial).initialize(address(123), "Eighteen Root Initial", "18RI", 18); + } + + function generateMapTokens() private { + MockAdaptor(mockAdaptorChild).onMessageReceive( + abi.encode( + ChildERC20Bridge(payable(childERC20Bridge)).MAP_TOKEN_SIG(), + rootSixDecimalInitial, + "Six Child Generated", + "6CG", + ChildERC20(rootSixDecimalInitial).decimals() + ) + ); + childSixDecimalGenerated = + RootERC20Bridge(payable(rootERC20BridgeFlowRate)).mapToken{value: 1}(IERC20Metadata(rootSixDecimalInitial)); + + // Verify that the token was mapped correctly + assert( + childSixDecimalGenerated + == ChildERC20Bridge(payable(childERC20Bridge)).rootTokenToChildToken(rootSixDecimalInitial) + ); + + MockAdaptor(mockAdaptorChild).onMessageReceive( + abi.encode( + ChildERC20Bridge(payable(childERC20Bridge)).MAP_TOKEN_SIG(), + rootEightDecimalInitial, + "Eight Child Generated", + "8CG", + ChildERC20(rootEightDecimalInitial).decimals() + ) + ); + childEightDecimalGenerated = RootERC20Bridge(payable(rootERC20BridgeFlowRate)).mapToken{value: 1}( + IERC20Metadata(rootEightDecimalInitial) + ); + + // Verify that the token was mapped correctly + assert( + childEightDecimalGenerated + == ChildERC20Bridge(payable(childERC20Bridge)).rootTokenToChildToken(rootEightDecimalInitial) + ); + + MockAdaptor(mockAdaptorChild).onMessageReceive( + abi.encode( + ChildERC20Bridge(payable(childERC20Bridge)).MAP_TOKEN_SIG(), + rootEighteenDecimalInitial, + "Eighteen Child Generated", + "18CG", + ChildERC20(rootEighteenDecimalInitial).decimals() + ) + ); + childEighteenDecimalGenerated = RootERC20Bridge(payable(rootERC20BridgeFlowRate)).mapToken{value: 1}( + IERC20Metadata(rootEighteenDecimalInitial) + ); + + // Verify that the token was mapped correctly + assert( + childEighteenDecimalGenerated + == ChildERC20Bridge(payable(childERC20Bridge)).rootTokenToChildToken(rootEighteenDecimalInitial) + ); + + childETHToken = ChildERC20Bridge(payable(childERC20Bridge)).childETHToken(); + assert(childETHToken != address(0)); + } + + function addTokensToArrays() private { + allTokens.push(rootSixDecimalInitial); + allTokens.push(rootEightDecimalInitial); + allTokens.push(rootEighteenDecimalInitial); + allTokens.push(childSixDecimalGenerated); + allTokens.push(childEightDecimalGenerated); + allTokens.push(childEighteenDecimalGenerated); + allTokens.push(rootIMXToken); + allTokens.push(childETHToken); + allTokens.push(wETH); + allTokens.push(wIMX); + + rootTokens.push(rootSixDecimalInitial); + rootTokens.push(rootEightDecimalInitial); + rootTokens.push(rootEighteenDecimalInitial); + rootTokens.push(rootIMXToken); + rootTokens.push(wETH); + + childTokens.push(childSixDecimalGenerated); + childTokens.push(childEightDecimalGenerated); + childTokens.push(childEighteenDecimalGenerated); + childTokens.push(childETHToken); + childTokens.push(wIMX); + } + + function addAddressesToTargets() private { + targets.push(childERC20Bridge); + targets.push(rootERC20BridgeFlowRate); + targets.push(mockAdaptorRoot); + targets.push(mockAdaptorChild); + } +} diff --git a/test/fuzzing/Properties.md b/test/fuzzing/Properties.md new file mode 100644 index 00000000..cd3c8191 --- /dev/null +++ b/test/fuzzing/Properties.md @@ -0,0 +1,69 @@ +| Property | Description | Tested | Passed | Number of Runs | +| --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | ------ | -------------- | +| FLRT-01 | If imxCumulativeDepositLimit is not 0, the rootIMX token balance of the root bridge should always be less than or equal to imxCumulativeDepositLimit | ✅ | ✅ | 1.41B+ | +| FLRT-02 | If the withdrawal amount is greater than the largeTransferThreshold, the withdrawal must be added to the user's withdrawal queue | ✅ | ✅ | 1.41B+ | +| FLRT-03 | If the bucket capacity for a token is 0, then any withdrawal of that token must be added to the user's withdrawal queue | ✅ | ✅ | 1.41B+ | +| FLRT-04 | If withdrawalQueueActivated is true, then any withdrawal of any token must be added to the user's withdrawal queue | ✅ | ✅ | 1.41B+ | +| FLRT-05 | If finaliseQueuedWithdrawal is successfully called, then the queued withdrawal initiated timestamp plus the withdrawal delay must be less than or equal to the current block timestamp | ✅ | ✅ | 1.41B+ | +| FLRT-06 | After a successful withdrawal of a non-zero amount on the root bridge, if the bucket capacity is not zero, then the token bucket depth is never equal to the bucket capacity | ✅ | ✅ | 1.41B+ | +| FLRT-07 | After a successful withdraw on the root bridge, if the withdrawn amount is less than the minimum of the refill rate multiplied by the time elapsed since last withdrawal and the bucket capacity minus the prior real bucket depth, then the bucket depth strictly increases | ✅ | ✅ | 1.41B+ | +| FLRT-08 | After a successful withdraw on the root bridge, the bucket depth is always less than or equal to the bucket capacity | ✅ | ✅ | 1.41B+ | +| FLRT-09 | When the withdrawalQueue is not activated, after a successful withdrawal of an amount less than the largeTransferThreshold on the root bridge and a withdrawal was added to the user's withdrawal queue, the bucket depth is always 0 | ✅ | ✅ | 1.41B+ | +| FLRT-10 | When the withdrawalQueue is not activated, after a successful withdrawal of an amount less than the largeTransferThreshold on the root bridge, if the withdrawn amount is greater or equal to minimum of the refill rate multiplied by the time elapsed since last withdrawal plus the prior real bucket depth and the bucket capacity, then the withdrawal must be added to the user's withdrawal queue | ✅ | ✅ | 1.41B+ | +| FLRT-11 | If the depth of the token bucket is non-zero before a successful withdrawal on the root bridge and the depth of the token bucket is zero after the withdrawal, then the withdrawal queue must be activated | ✅ | ✅ | 1.41B+ | +| FLRT-12 | After each successful withdrawal on the root bridge for a token with a non-zero capacity, the bucket refillTime is always updated to the current block timestamp | ✅ | ✅ | 1.41B+ | +| SPLY-01 | The sum of the token balances of each user and the bridges should be exactly equal to the token total supply | ✅ | ✅ | 1.41B+ | +| SPLY-02 | The total supply of root WETH must be decreased by the amount of WETH deposited by the user | ✅ | ✅ | 1.41B+ | +| SPLY-03 | The total supply of child ERC20s must be increased by exactly the amount of tokens deposited by the user | ✅ | ✅ | 1.41B+ | +| SPLY-04 | The total supply of child ERC20s must be decreased by exactly the amount of tokens withdrawn by the user | ✅ | ✅ | 1.41B+ | +| BAL-01 | The WETH balance of the root bridge should always be 0 | ✅ | ✅ | 1.41B+ | +| BAL-02 | The WIMX balance of the child bridge should always be 0 | ✅ | ✅ | 1.41B+ | +| BAL-03 | The native balance of the root adaptor increases by exactly the gas fees paid by the users | ✅ | ✅ | 1.41B+ | +| BAL-04 | The native balance of the child adaptor increases by exactly the gas fees paid by the users | ✅ | ✅ | 1.41B+ | +| BAL-05 | The token balance, excluding WETH, of the root bridge increases by exactly the amount of tokens deposited by the user | ✅ | ✅ | 1.41B+ | +| BAL-06 | The native balance of the root bridge increases by exactly the amount of WETH deposited by the user | ✅ | ✅ | 1.41B+ | +| BAL-07 | When depositing ETH, the native balance of the root bridge increases by exactly the amount deposited by the user, minus the gas fees | ✅ | ✅ | 1.41B+ | +| BAL-08 | When withdrawing IMX, the native balance of the child bridge increases by exactly the amount withdrawn by the user, minus the gas fees | ✅ | ✅ | 1.41B+ | +| BAL-09 | The native balance of the child bridge decreases by exactly the amount of root ERC20 IMX deposited by the user | ✅ | ✅ | 1.41B+ | +| RTREV-01 | activateWithdrawalQueue never reverts | ✅ | ✅ | 1.41B+ | +| RTREV-02 | deactivateWithdrawalQueue never reverts | ✅ | ✅ | 1.41B+ | +| RTREV-03 | deposit never reverts unexpectedly | ✅ | ✅ | 1.41B+ | +| RTREV-04 | depositETH never reverts unexpectedly | ✅ | ✅ | 1.41B+ | +| RTREV-05 | depositTo never reverts unexpectedly | ✅ | ✅ | 1.41B+ | +| RTREV-06 | depositToETH never reverts unexpectedly | ✅ | ✅ | 1.41B+ | +| RTREV-07 | finaliseQueuedWithdrawal never reverts unexpectedly | ✅ | ✅ | 1.41B+ | +| RTREV-08 | finaliseQueuedWithdrawalAggregated never reverts unexpectedly | ✅ | ✅ | 1.41B+ | +| RTREV-09 | onMessageReceive for the root bridge never reverts unexpectedly | ✅ | ✅ | 1.41B+ | +| RTREV-10 | pause for the root bridge never reverts | ✅ | ✅ | 1.41B+ | +| RTREV-11 | setRateControlThreshold never reverts | ✅ | ✅ | 1.41B+ | +| RTREV-12 | setWithdrawalDelay never reverts | ✅ | ✅ | 1.41B+ | +| RTREV-13 | unpause for the root bridge never reverts | ✅ | ✅ | 1.41B+ | +| RTREV-14 | updateImxCumulativeDepositLimit never reverts unexpectedly | ✅ | ✅ | 1.41B+ | +| CLDREV-01 | onMessageReceive for the child bridge never reverts unexpectedly | ✅ | ✅ | 1.41B+ | +| CLDREV-02 | pause for the child bridge never reverts | ✅ | ✅ | 1.41B+ | +| CLDREV-03 | unpause for the child bridge never reverts | ✅ | ✅ | 1.41B+ | +| CLDREV-04 | withdraw never reverts unexpectedly | ✅ | ✅ | 1.41B+ | +| CLDREV-05 | withdrawETH never reverts unexpectedly | ✅ | ✅ | 1.41B+ | +| CLDREV-06 | withdrawETHTo never reverts unexpectedly | ✅ | ✅ | 1.41B+ | +| CLDREV-07 | withdrawIMX never reverts unexpectedly | ✅ | ✅ | 1.41B+ | +| CLDREV-08 | withdrawIMXTo never reverts unexpectedly | ✅ | ✅ | 1.41B+ | +| CLDREV-09 | withdrawTo never reverts unexpectedly | ✅ | ✅ | 1.41B+ | +| CLDREV-10 | withdrawWIMX never reverts unexpectedly | ✅ | ✅ | 1.41B+ | +| CLDREV-11 | withdrawWIMXTo never reverts unexpectedly | ✅ | ✅ | 1.41B+ | +| PAUSE-01 | deposit always reverts when the root bridge is paused | ✅ | ✅ | 1.41B+ | +| PAUSE-02 | depositETH always reverts when the root bridge is paused | ✅ | ✅ | 1.41B+ | +| PAUSE-03 | depositTo always reverts when the root bridge is paused | ✅ | ✅ | 1.41B+ | +| PAUSE-04 | depositToETH always reverts when the root bridge is paused | ✅ | ✅ | 1.41B+ | +| PAUSE-05 | finaliseQueuedWithdrawal always reverts when the root bridge is paused | ✅ | ✅ | 1.41B+ | +| PAUSE-06 | finaliseQueuedWithdrawalAggregated always reverts when the root bridge is paused | ✅ | ✅ | 1.41B+ | +| PAUSE-07 | onMessageReceive for the root bridge always reverts when the root bridge is paused | ✅ | ✅ | 1.41B+ | +| PAUSE-08 | withdraw always reverts when the child bridge is paused | ✅ | ✅ | 1.41B+ | +| PAUSE-09 | withdrawETH always reverts when the child bridge is paused | ✅ | ✅ | 1.41B+ | +| PAUSE-10 | withdrawETHTo always reverts when the child bridge is paused | ✅ | ✅ | 1.41B+ | +| PAUSE-11 | withdrawIMX always reverts when the child bridge is paused | ✅ | ✅ | 1.41B+ | +| PAUSE-12 | withdrawIMXTo always reverts when the child bridge is paused | ✅ | ✅ | 1.41B+ | +| PAUSE-13 | withdrawTo always reverts when the child bridge is paused | ✅ | ✅ | 1.41B+ | +| PAUSE-14 | withdrawWIMX always reverts when the child bridge is paused | ✅ | ✅ | 1.41B+ | +| PAUSE-15 | withdrawWIMXTo always reverts when the child bridge is paused | ✅ | ✅ | 1.41B+ | +| PAUSE-16 | onMessageReceive for the child bridge always reverts when the child bridge is paused | ✅ | ✅ | 1.41B+ | + \ No newline at end of file diff --git a/test/fuzzing/README.md b/test/fuzzing/README.md new file mode 100644 index 00000000..20e16ea8 --- /dev/null +++ b/test/fuzzing/README.md @@ -0,0 +1,51 @@ +## Overview + +Immutable engaged [Perimeter](https://www.perimetersec.io) for an in-depth invariant suite for the Immutable zkEVM Bridge. The focus of this engagement was to replicate all of the invariants that made sense to be replicated from the existing foundry suite to an Echidna/Medusa invariant suite. Additionally, many more invariants were added with a emphasis on the flow rate mechanism. This engagement was conducted by [@0xScourgedev](https://twitter.com/0xScourgedev), and concluded on September 1st, 2024. The fuzzing suite was successfully delivered upon the engagement's conclusion. + +## Contents + +This fuzzing suite was created for the scope below. The primary objective of this engagement was to replicate the invariants from the existing Foundry suite into an Echidna/Medusa invariant suite for long-term use. Additionally, the engagement aimed to incorporate numerous new invariants, many related to the root bridge's flow rate, to conduct thorough stress testing of the contracts and ensure high coverage rates for the invariant suite. + +The invariant suite developed for this engagement tests each side of the bridge individually with a superset of expected values to thoroughly test for edge cases and to reach additional coverage. Fuzzing across the root bridge and the child bridge is out of scope of this engagement and can be addressed in a follow-up engagement. + +All properties tested can be found in `Properties.md`. + +## Setup + +1. Installing Echidna and Medusa + + a. Install Echidna, follow the steps here: [Installation Guide](https://github.com/crytic/echidna#installation) using the latest master branch + + b. Install Medusa, follow the steps here: [Installation Guide](https://github.com/crytic/medusa/blob/master/docs/src/getting_started/installation.md) + +2. Install dependencies with `yarn install` + +3. Running the Fuzzing Invariants + + a. To fuzz all invariants using Echidna, run the command: + ``` + echidna . --contract Fuzz --config echidna-config.yaml + ``` + + b. To fuzz all invariants using Medusa, run the command: + ``` + medusa fuzz + ``` + +## Scope + +Repo: [https://github.com/immutable/zkevm-bridge-contracts](https://github.com/immutable/zkevm-bridge-contracts) + +Branch: `main` + +Commit: `0a56c7cf3675ff9c71010d2d768da4bf1dd7f204` + +``` +src/child/ChildERC20Bridge.sol +src/child/ChildERC20.sol +src/child/WIMX.sol +src/root/flowrate/RootERC20BridgeFlowRate.sol +src/root/flowrate/FlowRateWithdrawalQueue.sol +src/root/flowrate/FlowRateDetection.sol +src/root/RootERC20Bridge.sol +``` diff --git a/test/fuzzing/helper/BeforeAfter.sol b/test/fuzzing/helper/BeforeAfter.sol new file mode 100644 index 00000000..9e00d858 --- /dev/null +++ b/test/fuzzing/helper/BeforeAfter.sol @@ -0,0 +1,166 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.0; + +import "../FuzzSetup.sol"; + +/** + * @title BeforeAfter + * @author 0xScourgedev + * @notice Contains the states of the system before and after calls + */ +abstract contract BeforeAfter is FuzzSetup { + /////////////////////////////////////////////////////////////////////////////////////////////// + // STRUCTS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + struct State { + // actor => actorStates + mapping(address => ActorStates) actorStates; + // token => tokenState + mapping(address => TokenState) tokenStates; + uint256 imxCumulativeDepositLimit; + uint256 rootIMXTokenBalOfRootBridge; + uint256 rootWETHBalOfRootBridge; + uint256 childWIMXBalOfChildBridge; + uint256 nativeBalanceOfRootAdaptor; + uint256 nativeBalanceOfChildAdaptor; + uint256 nativeBalanceOfRootBridge; + uint256 nativeBalanceOfChildBridge; + uint256 withdrawalDelay; + bool rootBridgePaused; + bool childBridgePaused; + bool withdrawalQueueActivated; + } + + struct ActorStates { + uint256 queueLength; + } + + struct TokenState { + // address => balance + mapping(address => uint256) balances; + uint256 totalSupply; + uint256 capacity; + uint256 depth; + uint256 refillTime; + uint256 refillRate; + uint256 largeTransferThresholds; + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // VARIABLES // + /////////////////////////////////////////////////////////////////////////////////////////////// + + // callNum => State + mapping(uint8 => State) states; + + /////////////////////////////////////////////////////////////////////////////////////////////// + // FUNCTIONS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + function _before(address[] memory actors, address tokenToUpdate) internal { + _setStates(0, actors, tokenToUpdate); + + if (DEBUG) debugBefore(actors, tokenToUpdate); + } + + function _after(address[] memory actors, address tokenToUpdate) internal { + _setStates(1, actors, tokenToUpdate); + + if (DEBUG) debugAfter(actors, tokenToUpdate); + } + + function _setStates(uint8 callNum, address[] memory actors, address tokenToUpdate) internal { + for (uint256 i = 0; i < actors.length; i++) { + _setActorState(callNum, actors[i], tokenToUpdate); + } + + _setTokenState(callNum, tokenToUpdate); + + states[callNum].imxCumulativeDepositLimit = + RootERC20Bridge(payable(rootERC20BridgeFlowRate)).imxCumulativeDepositLimit(); + states[callNum].rootIMXTokenBalOfRootBridge = getBalance(rootIMXToken, rootERC20BridgeFlowRate); + states[callNum].rootWETHBalOfRootBridge = getBalance(wETH, rootERC20BridgeFlowRate); + states[callNum].childWIMXBalOfChildBridge = getBalance(wIMX, childERC20Bridge); + states[callNum].nativeBalanceOfRootAdaptor = mockAdaptorRoot.balance; + states[callNum].nativeBalanceOfChildAdaptor = mockAdaptorChild.balance; + + states[callNum].nativeBalanceOfRootBridge = rootERC20BridgeFlowRate.balance; + states[callNum].nativeBalanceOfChildBridge = childERC20Bridge.balance; + + states[callNum].withdrawalDelay = FlowRateWithdrawalQueue(payable(rootERC20BridgeFlowRate)).withdrawalDelay(); + + states[callNum].rootBridgePaused = RootERC20Bridge(payable(rootERC20BridgeFlowRate)).paused(); + states[callNum].childBridgePaused = ChildERC20Bridge(payable(childERC20Bridge)).paused(); + + states[callNum].withdrawalQueueActivated = + FlowRateDetection(payable(rootERC20BridgeFlowRate)).withdrawalQueueActivated(); + } + + function _setActorState(uint8 callNum, address actor, address tokenToUpdate) internal { + states[callNum].actorStates[actor].queueLength = + FlowRateWithdrawalQueue(payable(rootERC20BridgeFlowRate)).getPendingWithdrawalsLength(actor); + } + + function _setTokenState(uint8 callNum, address tokenToUpdate) internal { + if (tokenToUpdate == address(0)) return; + + if (tokenToUpdate != NATIVE_ETH) { + states[callNum].tokenStates[tokenToUpdate].totalSupply = ChildERC20(tokenToUpdate).totalSupply(); + } + + for (uint256 i = 0; i < USERS.length; i++) { + states[callNum].tokenStates[tokenToUpdate].balances[USERS[i]] = getBalance(tokenToUpdate, USERS[i]); + } + + states[callNum].tokenStates[tokenToUpdate].balances[rootERC20BridgeFlowRate] = + getBalance(tokenToUpdate, rootERC20BridgeFlowRate); + states[callNum].tokenStates[tokenToUpdate].balances[childERC20Bridge] = + getBalance(tokenToUpdate, childERC20Bridge); + + (uint256 capacity, uint256 depth, uint256 refillTime, uint256 refillRate) = + FlowRateDetection(rootERC20BridgeFlowRate).flowRateBuckets(tokenToUpdate); + states[callNum].tokenStates[tokenToUpdate].capacity = capacity; + states[callNum].tokenStates[tokenToUpdate].depth = depth; + states[callNum].tokenStates[tokenToUpdate].refillTime = refillTime; + states[callNum].tokenStates[tokenToUpdate].refillRate = refillRate; + + states[callNum].tokenStates[tokenToUpdate].largeTransferThresholds = + RootERC20BridgeFlowRate(payable(rootERC20BridgeFlowRate)).largeTransferThresholds(tokenToUpdate); + } + + function debugBefore(address[] memory actors, address tokenToUpdate) internal { + debugState(0, actors, tokenToUpdate); + } + + function debugAfter(address[] memory actors, address tokenToUpdate) internal { + debugState(1, actors, tokenToUpdate); + } + + function debugState(uint8 callNum, address[] memory actors, address tokenToUpdate) internal { + for (uint256 i = 0; i < actors.length; i++) { + debugActorState(callNum, actors[i]); + } + + debugTokenState(callNum, tokenToUpdate); + } + + function debugActorState(uint8 callNum, address actor) internal {} + + function debugTokenState(uint8 callNum, address tokenToUpdate) internal { + fl.log("Token address: ", tokenToUpdate); + if (tokenToUpdate == address(0)) return; + + if (tokenToUpdate != NATIVE_ETH) { + fl.log("Token: ", ChildERC20(tokenToUpdate).name()); + } + } + + function getBalance(address token, address user) internal returns (uint256) { + if (token == NATIVE_ETH) { + return user.balance; + } else { + return ChildERC20(token).balanceOf(user); + } + } +} diff --git a/test/fuzzing/helper/FuzzStorageVariables.sol b/test/fuzzing/helper/FuzzStorageVariables.sol new file mode 100644 index 00000000..71081401 --- /dev/null +++ b/test/fuzzing/helper/FuzzStorageVariables.sol @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.0; + +import "../util/FuzzConstants.sol"; +import "../../../src/child/ChildERC20Bridge.sol"; +import "../../../src/root/flowrate/RootERC20BridgeFlowRate.sol"; +import "../../../src/common/BridgeRoles.sol"; +import "../../../src/root/RootERC20Bridge.sol"; +import "../../../src/child/ChildERC20.sol"; +import "../mocks/MockAdaptor.sol"; +import "../../../src/lib/WETH.sol"; +import "../../../src/child/WIMX.sol"; + +/** + * @title FuzzStorageVariables + * @author 0xScourgedev + * @notice Contains all of the storage variables for the fuzzing suite + */ +abstract contract FuzzStorageVariables is FuzzConstants { + /////////////////////////////////////////////////////////////////////////////////////////////// + // STRUCTS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /////////////////////////////////////////////////////////////////////////////////////////////// + // FUZZ SETTINGS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + // Turn this off for PoC functions + bool internal _setActor = true; + + // Turn this to true if running optimization mode + bool internal constant OPTIMIZATION_ENABLED = false; + + // Turn this on to enable debug mode + bool internal constant DEBUG = false; + + /////////////////////////////////////////////////////////////////////////////////////////////// + // FUZZ VARIABLES // + /////////////////////////////////////////////////////////////////////////////////////////////// + + address internal currentActor; + + address[] internal allTokens; + address[] internal rootTokens; + address[] internal childTokens; + address[] internal targets; + + /////////////////////////////////////////////////////////////////////////////////////////////// + // DEPLOYED CONTRACTS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + address internal childERC20Bridge; + address internal rootERC20BridgeFlowRate; + address internal tokenTemplate; + address internal mockAdaptorRoot; + address internal mockAdaptorChild; + address internal rootIMXToken; + address internal wETH; + address internal wIMX; + address internal childETHToken; + + // Manually deployed token with six decimals on the root chain + address internal rootSixDecimalInitial; + // Generated token with mapToken with six decimals on the child chain + address internal childSixDecimalGenerated; + + // Manually deployed token with eight decimals on the root chain + address internal rootEightDecimalInitial; + // Generated token with mapToken with eight decimals on the child chain + address internal childEightDecimalGenerated; + + // Manually deployed token with eighteen decimals on the root chain + address internal rootEighteenDecimalInitial; + // Generated token with mapToken with eighteen decimals on the child chain + address internal childEighteenDecimalGenerated; +} diff --git a/test/fuzzing/helper/handlers/HandlerChildERC20Bridge.sol b/test/fuzzing/helper/handlers/HandlerChildERC20Bridge.sol new file mode 100644 index 00000000..a4faad89 --- /dev/null +++ b/test/fuzzing/helper/handlers/HandlerChildERC20Bridge.sol @@ -0,0 +1,230 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.0; + +import "../preconditions/PreconditionsChildERC20Bridge.sol"; +import "../postconditions/PostconditionsChildERC20Bridge.sol"; + +/** + * @title HandlerChildERC20Bridge + * @author 0xScourgedev + * @notice Fuzz handlers for ChildERC20Bridge + */ +contract HandlerChildERC20Bridge is PreconditionsChildERC20Bridge, PostconditionsChildERC20Bridge { + /////////////////////////////////////////////////////////////////////////////////////////////// + // HANDLERS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * @notice This handler calls the onMessageReceive function for the child bridge + * @param isDeposit If this is true, then the signature is for a deposit, otherwise it is for a invalid signature + * @param rootTokenSelector The selector for the root token to be deposited + * @param senderSelector The selector for the sender + * @param receiverSelector The selector for the receiver of the deposit + * @param amount The amount of tokens to be deposited + */ + function handler_onMessageReceiveChild( + bool isDeposit, + uint8 rootTokenSelector, + uint8 senderSelector, + uint8 receiverSelector, + uint256 amount + ) public setCurrentActor { + OnMessageReceiveChildParams memory params = + onMessageReceiveChildPreconditions(isDeposit, rootTokenSelector, senderSelector, receiverSelector, amount); + + address[] memory actorsToUpdate = new address[](1); + actorsToUpdate[0] = params.receiver; + + address childToken = ChildERC20Bridge(payable(childERC20Bridge)).rootTokenToChildToken(params.rootToken); + + _before(actorsToUpdate, childToken); + + (bool success, bytes memory returnData) = _onMessageReceiveChildCall(params.data); + + onMessageReceiveChildPostconditions(success, returnData, actorsToUpdate, childToken, params.amount); + } + + /** + * @notice This handler calls the pause function for the child bridge + */ + function handler_pauseChild() public setCurrentActor { + pauseChildPreconditions(); + + address[] memory actorsToUpdate = new address[](0); + + _before(actorsToUpdate, address(0)); + + (bool success, bytes memory returnData) = _pauseChildCall(); + + pauseChildPostconditions(success, returnData, actorsToUpdate, address(0)); + } + + /** + * @notice This handler calls the unpause function for the child bridge + */ + function handler_unpauseChild() public setCurrentActor { + unpauseChildPreconditions(); + + address[] memory actorsToUpdate = new address[](0); + + _before(actorsToUpdate, address(0)); + + (bool success, bytes memory returnData) = _unpauseChildCall(); + + unpauseChildPostconditions(success, returnData, actorsToUpdate, address(0)); + } + + /** + * @notice This handler calls the withdraw function for the child bridge + * @param childTokenSelector The selector for the child token to be withdrawn + * @param amount The amount of tokens to be withdrawn + * @param value The native token value of the transaction + */ + function handler_withdraw(uint8 childTokenSelector, uint256 amount, uint256 value) public setCurrentActor { + WithdrawParams memory params = withdrawPreconditions(childTokenSelector, amount, value); + + address[] memory actorsToUpdate = new address[](1); + actorsToUpdate[0] = currentActor; + + _before(actorsToUpdate, params.childToken); + + (bool success, bytes memory returnData) = _withdrawCall(params.childToken, params.amount, params.value); + + withdrawPostconditions(success, returnData, actorsToUpdate, params.childToken, params.amount, params.value); + } + + /** + * @notice This handler calls the withdrawETH function for the child bridge + * @param amount The amount of child ETH tokens to be withdrawn + * @param value The native token value of the transaction + */ + function handler_withdrawETH(uint256 amount, uint256 value) public setCurrentActor { + WithdrawETHParams memory params = withdrawETHPreconditions(amount, value); + + address[] memory actorsToUpdate = new address[](1); + actorsToUpdate[0] = currentActor; + + _before(actorsToUpdate, childETHToken); + + (bool success, bytes memory returnData) = _withdrawETHCall(params.amount, params.value); + + withdrawETHPostconditions(success, returnData, actorsToUpdate, childETHToken, params.amount, params.value); + } + + /** + * @notice This handler calls the withdrawETHTo function for the child bridge + * @param receiverSelector The selector for the receiver of the withdrawal + * @param amount The amount of child ETH tokens to be withdrawn + * @param value The native token value of the transaction + */ + function handler_withdrawETHTo(uint8 receiverSelector, uint256 amount, uint256 value) public setCurrentActor { + WithdrawETHToParams memory params = withdrawETHToPreconditions(receiverSelector, amount, value); + + address[] memory actorsToUpdate = new address[](1); + actorsToUpdate[0] = currentActor; + + _before(actorsToUpdate, childETHToken); + + (bool success, bytes memory returnData) = _withdrawETHToCall(params.receiver, params.amount, params.value); + + withdrawETHToPostconditions(success, returnData, actorsToUpdate, childETHToken, params.amount, params.value); + } + + /** + * @notice This handler calls the withdrawIMX function for the child bridge + * @param amount The amount of native tokens to be withdrawn + * @param value The native token value of the transaction + */ + function handler_withdrawIMX(uint256 amount, uint256 value) public setCurrentActor { + WithdrawIMXParams memory params = withdrawIMXPreconditions(amount, value); + + address[] memory actorsToUpdate = new address[](1); + actorsToUpdate[0] = currentActor; + + _before(actorsToUpdate, NATIVE_ETH); + + (bool success, bytes memory returnData) = _withdrawIMXCall(params.amount, params.value); + + withdrawIMXPostconditions(success, returnData, actorsToUpdate, NATIVE_ETH, params.amount, params.value); + } + + /** + * @notice This handler calls the withdrawIMXTo function for the child bridge + * @param receiverSelector The selector for the receiver of the withdrawal + * @param amount The amount of native tokens to be withdrawn + * @param value The native token value of the transaction + */ + function handler_withdrawIMXTo(uint8 receiverSelector, uint256 amount, uint256 value) public setCurrentActor { + WithdrawIMXToParams memory params = withdrawIMXToPreconditions(receiverSelector, amount, value); + + address[] memory actorsToUpdate = new address[](1); + actorsToUpdate[0] = currentActor; + + _before(actorsToUpdate, NATIVE_ETH); + + (bool success, bytes memory returnData) = _withdrawIMXToCall(params.receiver, params.amount, params.value); + + withdrawIMXToPostconditions(success, returnData, actorsToUpdate, NATIVE_ETH, params.amount, params.value); + } + + /** + * @notice This handler calls the withdrawTo function for the child bridge + * @param childTokenSelector The selector for the child token to be withdrawn + * @param receiverSelector The selector for the receiver of the withdrawal + * @param amount The amount of tokens to be withdrawn + * @param value The native token value of the transaction + */ + function handler_withdrawTo(uint8 childTokenSelector, uint8 receiverSelector, uint256 amount, uint256 value) + public + setCurrentActor + { + WithdrawToParams memory params = withdrawToPreconditions(childTokenSelector, receiverSelector, amount, value); + + address[] memory actorsToUpdate = new address[](1); + actorsToUpdate[0] = currentActor; + + _before(actorsToUpdate, params.childToken); + + (bool success, bytes memory returnData) = + _withdrawToCall(params.childToken, params.receiver, params.amount, params.value); + + withdrawToPostconditions(success, returnData, actorsToUpdate, params.childToken, params.amount, params.value); + } + + /** + * @notice This handler calls the withdrawWIMX function for the child bridge + * @param amount The amount of wIMX tokens to be withdrawn + * @param value The native token value of the transaction + */ + function handler_withdrawWIMX(uint256 amount, uint256 value) public setCurrentActor { + WithdrawWIMXParams memory params = withdrawWIMXPreconditions(amount, value); + + address[] memory actorsToUpdate = new address[](1); + actorsToUpdate[0] = currentActor; + + _before(actorsToUpdate, wIMX); + + (bool success, bytes memory returnData) = _withdrawWIMXCall(params.amount, params.value); + + withdrawWIMXPostconditions(success, returnData, actorsToUpdate, wIMX, params.amount, params.value); + } + + /** + * @notice This handler calls the withdrawWIMXTo function for the child bridge + * @param receiverSelector The selector for the receiver of the withdrawal + * @param amount The amount of wIMX tokens to be withdrawn + * @param value The native token value of the transaction + */ + function handler_withdrawWIMXTo(uint8 receiverSelector, uint256 amount, uint256 value) public setCurrentActor { + WithdrawWIMXToParams memory params = withdrawWIMXToPreconditions(receiverSelector, amount, value); + + address[] memory actorsToUpdate = new address[](1); + actorsToUpdate[0] = currentActor; + + _before(actorsToUpdate, wIMX); + + (bool success, bytes memory returnData) = _withdrawWIMXToCall(params.receiver, params.amount, params.value); + + withdrawWIMXToPostconditions(success, returnData, actorsToUpdate, wIMX, params.amount, params.value); + } +} diff --git a/test/fuzzing/helper/handlers/HandlerRootERC20BridgeFlowRate.sol b/test/fuzzing/helper/handlers/HandlerRootERC20BridgeFlowRate.sol new file mode 100644 index 00000000..d05a88ac --- /dev/null +++ b/test/fuzzing/helper/handlers/HandlerRootERC20BridgeFlowRate.sol @@ -0,0 +1,304 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.0; + +import "../preconditions/PreconditionsRootERC20BridgeFlowRate.sol"; +import "../postconditions/PostconditionsRootERC20BridgeFlowRate.sol"; + +/** + * @title HandlerRootERC20BridgeFlowRate + * @author 0xScourgedev + * @notice Fuzz handlers for RootERC20BridgeFlowRate + */ +contract HandlerRootERC20BridgeFlowRate is + PreconditionsRootERC20BridgeFlowRate, + PostconditionsRootERC20BridgeFlowRate +{ + /////////////////////////////////////////////////////////////////////////////////////////////// + // HANDLERS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * @notice This handler calls the activateWithdrawalQueue function for the root bridge + */ + function handler_activateWithdrawalQueue() public setCurrentActor { + activateWithdrawalQueuePreconditions(); + + address[] memory actorsToUpdate = new address[](0); + + _before(actorsToUpdate, address(0)); + + (bool success, bytes memory returnData) = _activateWithdrawalQueueCall(); + + activateWithdrawalQueuePostconditions(success, returnData, actorsToUpdate, address(0)); + } + + /** + * @notice This handler calls the deactivateWithdrawalQueue function for the root bridge + */ + function handler_deactivateWithdrawalQueue() public setCurrentActor { + deactivateWithdrawalQueuePreconditions(); + + address[] memory actorsToUpdate = new address[](0); + + _before(actorsToUpdate, address(0)); + + (bool success, bytes memory returnData) = _deactivateWithdrawalQueueCall(); + + deactivateWithdrawalQueuePostconditions(success, returnData, actorsToUpdate, address(0)); + } + + /** + * @notice This handler calls the deposit function for the root bridge + * @param rootTokenSelector The selector for the root token + * @param amount The amount of tokens to be deposited + * @param value The native token value of the transaction + */ + function handler_deposit(uint8 rootTokenSelector, uint256 amount, uint256 value) public setCurrentActor { + DepositParams memory params = depositPreconditions(rootTokenSelector, amount, value); + + address[] memory actorsToUpdate = new address[](1); + actorsToUpdate[0] = currentActor; + + _before(actorsToUpdate, params.rootToken); + + (bool success, bytes memory returnData) = _depositCall(params.rootToken, params.amount, params.value); + + depositPostconditions(success, returnData, actorsToUpdate, params.rootToken, params.amount, params.value); + } + + /** + * @notice This handler calls the depositETH function for the root bridge + * @param amount The amount of native tokens to be deposited + * @param value The native token value of the transaction + */ + function handler_depositETH(uint256 amount, uint256 value) public setCurrentActor { + DepositETHParams memory params = depositETHPreconditions(amount, value); + + address[] memory actorsToUpdate = new address[](1); + actorsToUpdate[0] = currentActor; + + _before(actorsToUpdate, NATIVE_ETH); + + (bool success, bytes memory returnData) = _depositETHCall(params.amount, params.value); + + depositETHPostconditions(success, returnData, actorsToUpdate, NATIVE_ETH, params.amount, params.value); + } + + /** + * @notice This handler calls the depositTo function for the root bridge + * @param rootTokenSelector The selector for the root token to deposit + * @param receiverSelector The selector for the receiver of the deposit + * @param amount The amount of tokens to be deposited + * @param value The native token value of the transaction + */ + function handler_depositTo(uint8 rootTokenSelector, uint8 receiverSelector, uint256 amount, uint256 value) + public + setCurrentActor + { + DepositToParams memory params = depositToPreconditions(rootTokenSelector, receiverSelector, amount, value); + + address[] memory actorsToUpdate = new address[](1); + actorsToUpdate[0] = currentActor; + + _before(actorsToUpdate, params.rootToken); + + (bool success, bytes memory returnData) = + _depositToCall(params.rootToken, params.receiver, params.amount, params.value); + + depositToPostconditions(success, returnData, actorsToUpdate, params.rootToken, params.amount, params.value); + } + + /** + * @notice This handler calls the depositToETH function for the root bridge + * @param receiverSelector The selector for the receiver of the deposit + * @param amount The amount of native tokens to be deposited + * @param value The native token value of the transaction + */ + function handler_depositToETH(uint8 receiverSelector, uint256 amount, uint256 value) public setCurrentActor { + DepositToETHParams memory params = depositToETHPreconditions(receiverSelector, amount, value); + + address[] memory actorsToUpdate = new address[](1); + actorsToUpdate[0] = currentActor; + + _before(actorsToUpdate, NATIVE_ETH); + + (bool success, bytes memory returnData) = _depositToETHCall(params.receiver, params.amount, params.value); + + depositToETHPostconditions(success, returnData, actorsToUpdate, NATIVE_ETH, params.amount, params.value); + } + + /** + * @notice This handler calls the finaliseQueuedWithdrawal function for the root bridge + * @param receiverSelector The selector for the receiver of the withdrawal + * @param index The index of the withdrawal to finalize + */ + function handler_finaliseQueuedWithdrawal(uint8 receiverSelector, uint256 index) public setCurrentActor { + FinaliseQueuedWithdrawalParams memory params = finaliseQueuedWithdrawalPreconditions(receiverSelector, index); + + address[] memory actorsToUpdate = new address[](1); + actorsToUpdate[0] = currentActor; + + uint256[] memory indices = new uint256[](1); + indices[0] = index; + + // If the withdrawal does not exist, then the before call will just get a zero address, and not do anything + FlowRateWithdrawalQueue.PendingWithdrawal[] memory withdrawals = + FlowRateWithdrawalQueue(rootERC20BridgeFlowRate).getPendingWithdrawals(params.receiver, indices); + + _before(actorsToUpdate, withdrawals[0].token); + + (bool success, bytes memory returnData) = _finaliseQueuedWithdrawalCall(params.receiver, params.index); + + finaliseQueuedWithdrawalPostconditions(success, returnData, actorsToUpdate, withdrawals[0]); + } + + /** + * @notice This handler calls the finaliseQueuedWithdrawalsAggregated function for the root bridge + * @param receiverSelector The selector for the receiver of the aggregated withdrawals + * @param tokenSelector The selector for the root token to finalize the withdrawals for + * @param length The length of the indices array to finalize for + * @param entropy The entropy to generate the indices from + */ + function handler_finaliseQueuedWithdrawalsAggregated( + uint8 receiverSelector, + uint8 tokenSelector, + uint256 length, + uint256 entropy + ) public setCurrentActor { + FinaliseQueuedWithdrawalsAggregatedParams memory params = + finaliseQueuedWithdrawalsAggregatedPreconditions(receiverSelector, tokenSelector, length, entropy); + + address[] memory actorsToUpdate = new address[](1); + actorsToUpdate[0] = currentActor; + + _before(actorsToUpdate, params.token); + + (bool success, bytes memory returnData) = + _finaliseQueuedWithdrawalsAggregatedCall(params.receiver, params.token, params.indices); + + finaliseQueuedWithdrawalsAggregatedPostconditions(success, returnData, actorsToUpdate, params.token); + } + + /** + * @notice This handler calls the onMessageReceive function for the root bridge + * @param isWithdraw If this is true, then the signature is for a withdraw, otherwise it is for a invalid signature + * @param rootTokenSelector The selector for the root token to be withdrawn + * @param withdrawerSelector The selector for the withdrawer + * @param receiverSelector The selector for the receiver of the withdrawal + * @param amount The amount of tokens to be withdrawn + */ + function handler_onMessageReceiveRoot( + bool isWithdraw, + uint8 rootTokenSelector, + uint8 withdrawerSelector, + uint8 receiverSelector, + uint256 amount + ) public setCurrentActor { + OnMessageReceiveRootParams memory params = onMessageReceiveRootPreconditions( + isWithdraw, rootTokenSelector, withdrawerSelector, receiverSelector, amount + ); + + address[] memory actorsToUpdate = new address[](1); + actorsToUpdate[0] = params.receiver; + + _before(actorsToUpdate, params.rootToken); + + (bool success, bytes memory returnData) = _onMessageReceiveRootCall(params.data); + + onMessageReceiveRootPostconditions( + success, returnData, actorsToUpdate, params.rootToken, params.amount, params.receiver + ); + } + + /** + * @notice This handler calls the pause function for the root bridge + */ + function handler_pauseRoot() public setCurrentActor { + pauseRootPreconditions(); + + address[] memory actorsToUpdate = new address[](1); + actorsToUpdate[0] = currentActor; + + _before(actorsToUpdate, address(0)); + + (bool success, bytes memory returnData) = _pauseRootCall(); + + pauseRootPostconditions(success, returnData, actorsToUpdate, address(0)); + } + + /** + * @notice This handler calls the setRateControlThreshold function for the root bridge + * @param tokenSelector The token selector for the token to set the rate control threshold for + * @param capacity The new capacity of the bucket for the selected token + * @param refillRate The new refill rate of the bucket for the selected token + * @param largeTransferThreshold The new large transfer threshold for the selected token + */ + function handler_setRateControlThreshold( + uint8 tokenSelector, + uint256 capacity, + uint256 refillRate, + uint256 largeTransferThreshold + ) public setCurrentActor { + SetRateControlThresholdParams memory params = + setRateControlThresholdPreconditions(tokenSelector, capacity, refillRate, largeTransferThreshold); + + address[] memory actorsToUpdate = new address[](0); + + _before(actorsToUpdate, params.token); + + (bool success, bytes memory returnData) = _setRateControlThresholdCall( + params.token, params.capacity, params.refillRate, params.largeTransferThreshold + ); + + setRateControlThresholdPostconditions(success, returnData, actorsToUpdate, params.token); + } + + /** + * @notice This handler calls the setWithdrawalDelay function for the root bridge + * @param delay The new withdrawal delay + */ + function handler_setWithdrawalDelay(uint256 delay) public setCurrentActor { + SetWithdrawalDelayParams memory params = setWithdrawalDelayPreconditions(delay); + + address[] memory actorsToUpdate = new address[](0); + + _before(actorsToUpdate, address(0)); + + (bool success, bytes memory returnData) = _setWithdrawalDelayCall(params.delay); + + setWithdrawalDelayPostconditions(success, returnData, actorsToUpdate, address(0)); + } + + /** + * @notice This handler calls the unpause function for the root bridge + */ + function handler_unpauseRoot() public setCurrentActor { + unpauseRootPreconditions(); + + address[] memory actorsToUpdate = new address[](0); + + _before(actorsToUpdate, address(0)); + + (bool success, bytes memory returnData) = _unpauseRootCall(); + + unpauseRootPostconditions(success, returnData, actorsToUpdate, address(0)); + } + + /** + * @notice This handler calls the updateImxCumulativeDepositLimit function for the root bridge + * @param newImxCumulativeDepositLimit The new cumulative deposit limit for IMX + */ + function handler_updateImxCumulativeDepositLimit(uint256 newImxCumulativeDepositLimit) public setCurrentActor { + UpdateImxCumulativeDepositLimitParams memory params = + updateImxCumulativeDepositLimitPreconditions(newImxCumulativeDepositLimit); + + address[] memory actorsToUpdate = new address[](0); + + _before(actorsToUpdate, address(0)); + + (bool success, bytes memory returnData) = + _updateImxCumulativeDepositLimitCall(params.newImxCumulativeDepositLimit); + + updateImxCumulativeDepositLimitPostconditions(success, returnData, actorsToUpdate, address(0)); + } +} diff --git a/test/fuzzing/helper/postconditions/PostconditionsBase.sol b/test/fuzzing/helper/postconditions/PostconditionsBase.sol new file mode 100644 index 00000000..55b2fdd7 --- /dev/null +++ b/test/fuzzing/helper/postconditions/PostconditionsBase.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.0; + +import "../../properties/Properties.sol"; + +/** + * @title PostconditionsBase + * @author 0xScourgedev + * @notice Contains general postconditions used across all postcondition contracts + */ +abstract contract PostconditionsBase is Properties { + /////////////////////////////////////////////////////////////////////////////////////////////// + // POSTCONDITIONS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * @notice invariants to run after each successful transaction + * @custom:invariant SPLY-01: The sum of the token balances of each user and the bridges should + * be exactly equal to the token total supply + */ + function onSuccessInvariantsGeneral(bytes memory returnData, address tokenToCheck) internal { + invariant_SPLY_01(tokenToCheck); + } + + /** + * @notice invariants to run after each failed transaction + */ + function onFailInvariantsGeneral(bytes memory returnData) internal {} +} diff --git a/test/fuzzing/helper/postconditions/PostconditionsChildERC20Bridge.sol b/test/fuzzing/helper/postconditions/PostconditionsChildERC20Bridge.sol new file mode 100644 index 00000000..1d45f921 --- /dev/null +++ b/test/fuzzing/helper/postconditions/PostconditionsChildERC20Bridge.sol @@ -0,0 +1,342 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.0; + +import "./PostconditionsBase.sol"; + +/** + * @title PostconditionsChildERC20Bridge + * @author 0xScourgedev + * @notice Contains all postconditions for ChildERC20Bridge + */ +abstract contract PostconditionsChildERC20Bridge is PostconditionsBase { + /////////////////////////////////////////////////////////////////////////////////////////////// + // POSTCONDITIONS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * @notice Postconditions for the onMessageReceive function for the child bridge + * @custom:invariant PAUSE-16: onMessageReceive for the child bridge always reverts when the child bridge is paused + * @custom:invariant SPLY-03: The total supply of child ERC20s must be increased by exactly + * the amount of tokens deposited by the user + * + * @custom:invariant BAL-09: The native balance of the child bridge decreases by exactly the + * amount of root ERC20 IMX deposited by the user + */ + function onMessageReceiveChildPostconditions( + bool success, + bytes memory returnData, + address[] memory actorsToUpdate, + address tokenToUpdate, + uint256 amount + ) internal { + if (success) { + _after(actorsToUpdate, tokenToUpdate); + invariant_PAUSE_16(); + invariant_SPLY_03(tokenToUpdate, amount); + invariant_BAL_09(tokenToUpdate, amount); + onSuccessInvariantsGeneral(returnData, tokenToUpdate); + onSuccessInvariantsChildERC20Bridge(returnData); + } else { + invariant_CLDREV_01(returnData); + onFailInvariantsGeneral(returnData); + onFailInvariantsChildERC20Bridge(returnData); + } + } + + /** + * @notice Postconditions for the pause function for the child bridge + * @custom:invariant CLDREV-02: pause for the child bridge never reverts + */ + function pauseChildPostconditions( + bool success, + bytes memory returnData, + address[] memory actorsToUpdate, + address tokenToUpdate + ) internal { + if (success) { + _after(actorsToUpdate, tokenToUpdate); + onSuccessInvariantsGeneral(returnData, tokenToUpdate); + onSuccessInvariantsChildERC20Bridge(returnData); + } else { + invariant_CLDREV_02(returnData); + onFailInvariantsGeneral(returnData); + onFailInvariantsChildERC20Bridge(returnData); + } + } + + /** + * @notice Postconditions for the unpause function for the child bridge + * @custom:invariant CLDREV-03: unpause for the child bridge never reverts + */ + function unpauseChildPostconditions( + bool success, + bytes memory returnData, + address[] memory actorsToUpdate, + address tokenToUpdate + ) internal { + if (success) { + _after(actorsToUpdate, tokenToUpdate); + onSuccessInvariantsGeneral(returnData, tokenToUpdate); + onSuccessInvariantsChildERC20Bridge(returnData); + } else { + invariant_CLDREV_03(returnData); + onFailInvariantsGeneral(returnData); + onFailInvariantsChildERC20Bridge(returnData); + } + } + + /** + * @notice Postconditions for the withdraw function for the child bridge + * @custom:invariant PAUSE-08: withdraw always reverts when the child bridge is paused + * @custom:invariant BAL-04: The native balance of the child adaptor increases by exactly the gas fees paid by the users + * @custom:invariant SPLY-04: The total supply of child ERC20s must be decreased by exactly the + * amount of tokens withdrawn by the user + * + * @custom:invariant CLDREV-04: withdraw never reverts unexpectedly + */ + function withdrawPostconditions( + bool success, + bytes memory returnData, + address[] memory actorsToUpdate, + address tokenToUpdate, + uint256 amount, + uint256 value + ) internal { + if (success) { + _after(actorsToUpdate, tokenToUpdate); + invariant_PAUSE_08(); + invariant_BAL_04(value); + invariant_SPLY_04(tokenToUpdate, amount); + onSuccessInvariantsGeneral(returnData, tokenToUpdate); + onSuccessInvariantsChildERC20Bridge(returnData); + } else { + invariant_CLDREV_04(returnData); + onFailInvariantsGeneral(returnData); + onFailInvariantsChildERC20Bridge(returnData); + } + } + + /** + * @notice Postconditions for the withdrawETH function for the child bridge + * @custom:invariant PAUSE-09: withdrawETH always reverts when the child bridge is paused + * @custom:invariant BAL-04: The native balance of the child adaptor increases by exactly the gas fees paid by the users + * @custom:invariant SPLY-04: The total supply of child ERC20s must be decreased by exactly the + * amount of tokens withdrawn by the user + * @custom:invariant CLDREV-05: withdrawETH never reverts unexpectedly + */ + function withdrawETHPostconditions( + bool success, + bytes memory returnData, + address[] memory actorsToUpdate, + address tokenToUpdate, + uint256 amount, + uint256 value + ) internal { + if (success) { + _after(actorsToUpdate, tokenToUpdate); + invariant_PAUSE_09(); + invariant_BAL_04(value); + invariant_SPLY_04(tokenToUpdate, amount); + onSuccessInvariantsGeneral(returnData, tokenToUpdate); + onSuccessInvariantsChildERC20Bridge(returnData); + } else { + invariant_CLDREV_05(returnData); + onFailInvariantsGeneral(returnData); + onFailInvariantsChildERC20Bridge(returnData); + } + } + + /** + * @notice Postconditions for the withdrawETHTo function for the child bridge + * @custom:invariant PAUSE-10: withdrawETHTo always reverts when the child bridge is paused + * @custom:invariant BAL-04: The native balance of the child adaptor increases by exactly the gas fees paid by the users + * @custom:invariant SPLY-04: The total supply of child ERC20s must be decreased by exactly the + * amount of tokens withdrawn by the user + * @custom:invariant CLDREV-06: withdrawETHTo never reverts unexpectedly + */ + function withdrawETHToPostconditions( + bool success, + bytes memory returnData, + address[] memory actorsToUpdate, + address tokenToUpdate, + uint256 amount, + uint256 value + ) internal { + if (success) { + _after(actorsToUpdate, tokenToUpdate); + invariant_PAUSE_10(); + invariant_BAL_04(value); + invariant_SPLY_04(tokenToUpdate, amount); + onSuccessInvariantsGeneral(returnData, tokenToUpdate); + onSuccessInvariantsChildERC20Bridge(returnData); + } else { + invariant_CLDREV_06(returnData); + onFailInvariantsGeneral(returnData); + onFailInvariantsChildERC20Bridge(returnData); + } + } + + /** + * @notice Postconditions for the withdrawIMX function for the child bridge + * @custom:invariant PAUSE-11: withdrawIMX always reverts when the child bridge is paused + * @custom:invariant BAL-04: The native balance of the child adaptor increases by exactly the gas fees paid by the users + * @custom:invariant BAL-08: When withdrawing IMX, the native balance of the child bridge increases + * by exactly the amount withdrawn by the user, minus the gas fees + * + * @custom:invariant CLDREV-07: withdrawIMX never reverts unexpectedly + */ + function withdrawIMXPostconditions( + bool success, + bytes memory returnData, + address[] memory actorsToUpdate, + address tokenToUpdate, + uint256 amount, + uint256 value + ) internal { + if (success) { + _after(actorsToUpdate, tokenToUpdate); + invariant_PAUSE_11(); + invariant_BAL_04(value - amount); + invariant_BAL_08(amount); + onSuccessInvariantsGeneral(returnData, tokenToUpdate); + onSuccessInvariantsChildERC20Bridge(returnData); + } else { + invariant_CLDREV_07(returnData); + onFailInvariantsGeneral(returnData); + onFailInvariantsChildERC20Bridge(returnData); + } + } + + /** + * @notice Postconditions for the withdrawIMXTo function for the child bridge + * @custom:invariant PAUSE-12: withdrawIMXTo always reverts when the child bridge is paused + * @custom:invariant BAL-04: The native balance of the child adaptor increases by exactly the gas fees paid by the users + * @custom:invariant CLDREV-08: withdrawIMXTo never reverts unexpectedly + */ + function withdrawIMXToPostconditions( + bool success, + bytes memory returnData, + address[] memory actorsToUpdate, + address tokenToUpdate, + uint256 amount, + uint256 value + ) internal { + if (success) { + _after(actorsToUpdate, tokenToUpdate); + invariant_PAUSE_12(); + invariant_BAL_04(value - amount); + onSuccessInvariantsGeneral(returnData, tokenToUpdate); + onSuccessInvariantsChildERC20Bridge(returnData); + } else { + invariant_CLDREV_08(returnData); + onFailInvariantsGeneral(returnData); + onFailInvariantsChildERC20Bridge(returnData); + } + } + + /** + * @notice Postconditions for the withdrawTo function for the child bridge + * @custom:invariant PAUSE-13: withdrawTo always reverts when the child bridge is paused + * @custom:invariant BAL-04: The native balance of the child adaptor increases by exactly the gas fees paid by the users + * @custom:invariant SPLY-04: The total supply of child ERC20s must be decreased by exactly the + * amount of tokens withdrawn by the user + * + * @custom:invariant CLDREV-09: withdrawTo never reverts unexpectedly + */ + function withdrawToPostconditions( + bool success, + bytes memory returnData, + address[] memory actorsToUpdate, + address tokenToUpdate, + uint256 amount, + uint256 value + ) internal { + if (success) { + _after(actorsToUpdate, tokenToUpdate); + invariant_PAUSE_13(); + invariant_BAL_04(value); + invariant_SPLY_04(tokenToUpdate, amount); + onSuccessInvariantsGeneral(returnData, tokenToUpdate); + onSuccessInvariantsChildERC20Bridge(returnData); + } else { + invariant_CLDREV_09(returnData); + onFailInvariantsGeneral(returnData); + onFailInvariantsChildERC20Bridge(returnData); + } + } + + /** + * @notice Postconditions for the withdrawWIMX function for the child bridge + * @custom:invariant PAUSE-14: withdrawWIMX always reverts when the child bridge is paused + * @custom:invariant BAL-04: The native balance of the child adaptor increases by exactly the gas fees paid by the users + * @custom:invariant SPLY-04: The total supply of child ERC20s must be decreased by exactly the + * amount of tokens withdrawn by the user + * + * @custom:invariant CLDREV-10: withdrawWIMX never reverts unexpectedly + */ + function withdrawWIMXPostconditions( + bool success, + bytes memory returnData, + address[] memory actorsToUpdate, + address tokenToUpdate, + uint256 amount, + uint256 value + ) internal { + if (success) { + _after(actorsToUpdate, tokenToUpdate); + invariant_PAUSE_14(); + invariant_BAL_04(value); + invariant_SPLY_04(tokenToUpdate, amount); + onSuccessInvariantsGeneral(returnData, tokenToUpdate); + onSuccessInvariantsChildERC20Bridge(returnData); + } else { + invariant_CLDREV_10(returnData); + onFailInvariantsGeneral(returnData); + onFailInvariantsChildERC20Bridge(returnData); + } + } + + /** + * @notice Postconditions for the withdrawWIMXTo function for the child bridge + * @custom:invariant PAUSE-15: withdrawWIMXTo always reverts when the child bridge is paused + * @custom:invariant BAL-04: The native balance of the child adaptor increases by exactly the gas fees paid by the users + * @custom:invariant SPLY-04: The total supply of child ERC20s must be decreased by exactly the + * amount of tokens withdrawn by the user + * + * @custom:invariant CLDREV-11: withdrawWIMXTo never reverts unexpectedly + */ + function withdrawWIMXToPostconditions( + bool success, + bytes memory returnData, + address[] memory actorsToUpdate, + address tokenToUpdate, + uint256 amount, + uint256 value + ) internal { + if (success) { + _after(actorsToUpdate, tokenToUpdate); + invariant_PAUSE_15(); + invariant_BAL_04(value); + invariant_SPLY_04(tokenToUpdate, amount); + onSuccessInvariantsGeneral(returnData, tokenToUpdate); + onSuccessInvariantsChildERC20Bridge(returnData); + } else { + invariant_CLDREV_11(returnData); + onFailInvariantsGeneral(returnData); + onFailInvariantsChildERC20Bridge(returnData); + } + } + + /** + * @notice invariants to run after each successful transaction for the child bridge + * @custom:invariant BAL-02: The WIMX balance of the child bridge should always be 0 + */ + function onSuccessInvariantsChildERC20Bridge(bytes memory returnData) internal { + invariant_BAL_02(); + } + + /** + * @notice invariants to run after each failed transaction for the child bridge + */ + function onFailInvariantsChildERC20Bridge(bytes memory returnData) internal {} +} diff --git a/test/fuzzing/helper/postconditions/PostconditionsRootERC20BridgeFlowRate.sol b/test/fuzzing/helper/postconditions/PostconditionsRootERC20BridgeFlowRate.sol new file mode 100644 index 00000000..9454b50c --- /dev/null +++ b/test/fuzzing/helper/postconditions/PostconditionsRootERC20BridgeFlowRate.sol @@ -0,0 +1,444 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.0; + +import "./PostconditionsBase.sol"; + +/** + * @title PostconditionsRootERC20BridgeFlowRate + * @author 0xScourgedev + * @notice Contains all postconditions for RootERC20BridgeFlowRate + */ +abstract contract PostconditionsRootERC20BridgeFlowRate is PostconditionsBase { + /////////////////////////////////////////////////////////////////////////////////////////////// + // POSTCONDITIONS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * @notice Postconditions for the activateWithdrawalQueue function for the root bridge + * @custom:invariant RTREV-01: activateWithdrawalQueue never reverts + */ + function activateWithdrawalQueuePostconditions( + bool success, + bytes memory returnData, + address[] memory actorsToUpdate, + address tokenToUpdate + ) internal { + if (success) { + _after(actorsToUpdate, tokenToUpdate); + onSuccessInvariantsGeneral(returnData, tokenToUpdate); + onSuccessInvariantsRootERC20BridgeFlowRate(returnData); + } else { + invariant_RTREV_01(returnData); + onFailInvariantsGeneral(returnData); + onFailInvariantsRootERC20BridgeFlowRate(returnData); + } + } + + /** + * @notice Postconditions for the deactivateWithdrawalQueue function for the root bridge + * @custom:invariant RTREV-02: deactivateWithdrawalQueue never reverts + */ + function deactivateWithdrawalQueuePostconditions( + bool success, + bytes memory returnData, + address[] memory actorsToUpdate, + address tokenToUpdate + ) internal { + if (success) { + _after(actorsToUpdate, tokenToUpdate); + onSuccessInvariantsGeneral(returnData, tokenToUpdate); + onSuccessInvariantsRootERC20BridgeFlowRate(returnData); + } else { + invariant_RTREV_02(returnData); + onFailInvariantsGeneral(returnData); + onFailInvariantsRootERC20BridgeFlowRate(returnData); + } + } + + /** + * @notice Postconditions for the deposit function for the root bridge + * @custom:invariant PAUSE-01: deposit always reverts when the root bridge is paused + * @custom:invariant BAL-03: The native balance of the root adaptor increases by exactly the + * gas fees paid by the users + * + * @custom:invariant BAL-05: The token balance, excluding WETH, of the root bridge increases + * by exactly the amount of tokens deposited by the user + * + * @custom:invariant BAL-06: The native balance of the root bridge increases by exactly the amount + * of WETH deposited by the user + * + * @custom:invariant SPLY-02: The total supply of root WETH must be decreased by the amount + * of WETH deposited by the user + * + * @custom:invariant RTREV-03: deposit never reverts unexpectedly + */ + function depositPostconditions( + bool success, + bytes memory returnData, + address[] memory actorsToUpdate, + address tokenToUpdate, + uint256 amount, + uint256 value + ) internal { + if (success) { + _after(actorsToUpdate, tokenToUpdate); + invariant_PAUSE_01(); + invariant_BAL_03(value); + invariant_BAL_05(tokenToUpdate, amount); + invariant_BAL_06(tokenToUpdate, amount); + invariant_SPLY_02(tokenToUpdate, amount); + onSuccessInvariantsGeneral(returnData, tokenToUpdate); + onSuccessInvariantsRootERC20BridgeFlowRate(returnData); + } else { + invariant_RTREV_03(returnData); + onFailInvariantsGeneral(returnData); + onFailInvariantsRootERC20BridgeFlowRate(returnData); + } + } + + /** + * @notice Postconditions for the depositETH function for the root bridge + * @custom:invariant PAUSE-02: depositETH always reverts when the root bridge is paused + * @custom:invariant BAL-03: The native balance of the root adaptor increases by exactly the gas fees paid by the users + * @custom:invariant BAL-07: When depositing ETH, the native balance of the root bridge increases by + * exactly the amount deposited by the user, minus the gas fees + * + * @custom:invariant RTREV-04: depositETH never reverts unexpectedly + */ + function depositETHPostconditions( + bool success, + bytes memory returnData, + address[] memory actorsToUpdate, + address tokenToUpdate, + uint256 amount, + uint256 value + ) internal { + if (success) { + _after(actorsToUpdate, tokenToUpdate); + invariant_PAUSE_02(); + invariant_BAL_03(value - amount); + invariant_BAL_07(amount); + onSuccessInvariantsGeneral(returnData, tokenToUpdate); + onSuccessInvariantsRootERC20BridgeFlowRate(returnData); + } else { + invariant_RTREV_04(returnData); + onFailInvariantsGeneral(returnData); + onFailInvariantsRootERC20BridgeFlowRate(returnData); + } + } + + /** + * @notice Postconditions for the depositTo function for the root bridge + * @custom:invariant PAUSE-03: depositTo always reverts when the root bridge is paused + * @custom:invariant BAL-03: The native balance of the root adaptor increases by exactly the gas fees paid by the users + * @custom:invariant BAL-05: The token balance, excluding WETH, of the root bridge increases by + * exactly the amount of tokens deposited by the user + * + * @custom:invariant BAL-06: The native balance of the root bridge increases by exactly the amount + * of WETH deposited by the user + * + * @custom:invariant SPLY-02: The total supply of root WETH must be decreased by the amount + * of WETH deposited by the user + * + * @custom:invariant RTREV-05: depositTo never reverts unexpectedly + */ + function depositToPostconditions( + bool success, + bytes memory returnData, + address[] memory actorsToUpdate, + address tokenToUpdate, + uint256 amount, + uint256 value + ) internal { + if (success) { + _after(actorsToUpdate, tokenToUpdate); + invariant_PAUSE_03(); + invariant_BAL_03(value); + invariant_BAL_05(tokenToUpdate, amount); + invariant_BAL_06(tokenToUpdate, amount); + invariant_SPLY_02(tokenToUpdate, amount); + onSuccessInvariantsGeneral(returnData, tokenToUpdate); + onSuccessInvariantsRootERC20BridgeFlowRate(returnData); + } else { + invariant_RTREV_05(returnData); + onFailInvariantsGeneral(returnData); + onFailInvariantsRootERC20BridgeFlowRate(returnData); + } + } + + /** + * @notice Postconditions for the depositToETH function for the root bridge + * @custom:invariant PAUSE-04: depositToETH always reverts when the root bridge is paused + * @custom:invariant BAL-03: The native balance of the root adaptor increases by exactly the gas fees paid by the users + * @custom:invariant BAL-07: When depositing ETH, the native balance of the root bridge increases by + * exactly the amount deposited by the user, minus the gas fees + * + * @custom:invariant RTREV-06: depositToETH never reverts unexpectedly + */ + function depositToETHPostconditions( + bool success, + bytes memory returnData, + address[] memory actorsToUpdate, + address tokenToUpdate, + uint256 amount, + uint256 value + ) internal { + if (success) { + _after(actorsToUpdate, tokenToUpdate); + invariant_PAUSE_04(); + invariant_BAL_03(value - amount); + invariant_BAL_07(amount); + onSuccessInvariantsGeneral(returnData, tokenToUpdate); + onSuccessInvariantsRootERC20BridgeFlowRate(returnData); + } else { + invariant_RTREV_06(returnData); + onFailInvariantsGeneral(returnData); + onFailInvariantsRootERC20BridgeFlowRate(returnData); + } + } + + /** + * @notice Postconditions for the finaliseQueuedWithdrawal function for the root bridge + * @custom:invariant PAUSE-05: finaliseQueuedWithdrawal always reverts when the root bridge is paused + * @custom:invariant FLRT-05: If finaliseQueuedWithdrawal is successfully called, then the queued + * withdrawal initiated timestamp plus the withdrawal delay must be less than or equal to the current block timestamp + * + * @custom:invariant RTREV-07: finaliseQueuedWithdrawal never reverts unexpectedly + */ + function finaliseQueuedWithdrawalPostconditions( + bool success, + bytes memory returnData, + address[] memory actorsToUpdate, + FlowRateWithdrawalQueue.PendingWithdrawal memory withdrawal + ) internal { + if (success) { + _after(actorsToUpdate, withdrawal.token); + invariant_PAUSE_05(); + invariant_FLRT_05(withdrawal); + onSuccessInvariantsGeneral(returnData, withdrawal.token); + onSuccessInvariantsRootERC20BridgeFlowRate(returnData); + } else { + invariant_RTREV_07(returnData); + onFailInvariantsGeneral(returnData); + onFailInvariantsRootERC20BridgeFlowRate(returnData); + } + } + + /** + * @notice Postconditions for the finaliseQueuedWithdrawalsAggregated function for the root bridge + * @custom:invariant PAUSE-06: finaliseQueuedWithdrawalsAggregated always reverts when the root bridge is paused + * @custom:invariant RTREV-08: finaliseQueuedWithdrawalsAggregated never reverts unexpectedly + */ + function finaliseQueuedWithdrawalsAggregatedPostconditions( + bool success, + bytes memory returnData, + address[] memory actorsToUpdate, + address tokenToUpdate + ) internal { + if (success) { + _after(actorsToUpdate, tokenToUpdate); + invariant_PAUSE_06(); + onSuccessInvariantsGeneral(returnData, tokenToUpdate); + onSuccessInvariantsRootERC20BridgeFlowRate(returnData); + } else { + invariant_RTREV_08(returnData); + onFailInvariantsGeneral(returnData); + onFailInvariantsRootERC20BridgeFlowRate(returnData); + } + } + + /** + * @notice Postconditions for the onMessageReceive function for the root bridge + * @custom:invariant PAUSE-07: onMessageReceive for the root bridge always reverts when the root bridge is paused + * @custom:invariant FLRT-02: If the withdrawal amount is greater than the largeTransferThreshold, + * the withdrawal must be added to the user's withdrawal queue + * + * @custom:invariant FLRT-03: If the bucket capacity for a token is 0, then any withdrawal of + * that token must be added to the user's withdrawal queue + * + * @custom:invariant FLRT-04: If withdrawalQueueActivated is true, then any withdrawal of + * any token must be added to the user's withdrawal queue + * + * @custom:invariant FLRT-06: After a successful withdrawal of a non-zero amount on the root + * bridge, if the bucket capacity is not zero, then the token bucket depth is never equal to + * the bucket capacity + * + * @custom:invariant FLRT-07: After a successful withdraw on the root bridge, if the + * withdrawn amount is less than the minimum of the refill rate multiplied by the time + * elapsed since last withdrawal and the bucket capacity minus the prior real bucket depth, + * then the bucket depth strictly increases + * + * @custom:invariant FLRT-08: After a successful withdraw on the root bridge, the bucket depth + * is always less than or equal to the bucket capacity + * + * @custom:invariant FLRT-09: When the withdrawalQueue is not activated, after a successful + * withdrawal of an amount less than the largeTransferThreshold on the root bridge and a withdrawal + * was added to the user's withdrawal queue, the bucket depth is always 0 + * + * @custom:invariant FLRT-10: When the withdrawalQueue is not activated, after a successful + * withdrawal of an amount less than the largeTransferThreshold on the root bridge, if the + * withdrawn amount is greater or equal to minimum of the refill rate multiplied by the time + * elapsed since last withdrawal plus the prior real bucket depth and the bucket capacity, + * then the withdrawal must be added to the user's withdrawal queue + * + * @custom:invariant FLRT-11: If the depth of the token bucket is non-zero before a successful + * withdrawal on the root bridge and the depth of the token bucket is zero after the withdrawal, + * then the withdrawal queue must be activated + * + * @custom:invariant FLRT-12: After each successful withdrawal on the root bridge for a token with + * a non-zero capacity, the bucket refillTime is always updated to the current block timestamp + * + * @custom:invariant RTREV-09: onMessageReceive for the root bridge never reverts unexpectedly + */ + function onMessageReceiveRootPostconditions( + bool success, + bytes memory returnData, + address[] memory actorsToUpdate, + address tokenToUpdate, + uint256 amount, + address recipient + ) internal { + if (success) { + _after(actorsToUpdate, tokenToUpdate); + invariant_PAUSE_07(); + invariant_FLRT_02(tokenToUpdate, recipient, amount); + invariant_FLRT_03(tokenToUpdate, recipient); + invariant_FLRT_04(recipient); + invariant_FLRT_06(tokenToUpdate, amount); + invariant_FLRT_07(tokenToUpdate, amount); + invariant_FLRT_08(tokenToUpdate); + invariant_FLRT_09(tokenToUpdate, recipient, amount); + invariant_FLRT_10(tokenToUpdate, recipient, amount); + invariant_FLRT_11(tokenToUpdate); + invariant_FLRT_12(tokenToUpdate); + onSuccessInvariantsGeneral(returnData, tokenToUpdate); + onSuccessInvariantsRootERC20BridgeFlowRate(returnData); + } else { + invariant_RTREV_09(returnData); + onFailInvariantsGeneral(returnData); + onFailInvariantsRootERC20BridgeFlowRate(returnData); + } + } + + /** + * @notice Postconditions for the pause function for the root bridge + * @custom:invariant RTREV-10: pause for the root bridge never reverts + */ + function pauseRootPostconditions( + bool success, + bytes memory returnData, + address[] memory actorsToUpdate, + address tokenToUpdate + ) internal { + if (success) { + _after(actorsToUpdate, tokenToUpdate); + onSuccessInvariantsGeneral(returnData, tokenToUpdate); + onSuccessInvariantsRootERC20BridgeFlowRate(returnData); + } else { + invariant_RTREV_10(returnData); + onFailInvariantsGeneral(returnData); + onFailInvariantsRootERC20BridgeFlowRate(returnData); + } + } + + /** + * @notice Postconditions for the setRateControlThreshold function for the root bridge + * @custom:invariant RTREV-11: setRateControlThreshold never reverts + */ + function setRateControlThresholdPostconditions( + bool success, + bytes memory returnData, + address[] memory actorsToUpdate, + address tokenToUpdate + ) internal { + if (success) { + _after(actorsToUpdate, tokenToUpdate); + onSuccessInvariantsGeneral(returnData, tokenToUpdate); + onSuccessInvariantsRootERC20BridgeFlowRate(returnData); + } else { + invariant_RTREV_11(returnData); + onFailInvariantsGeneral(returnData); + onFailInvariantsRootERC20BridgeFlowRate(returnData); + } + } + + /** + * @notice Postconditions for the setWithdrawalDelay function for the root bridge + * @custom:invariant RTREV-12: setWithdrawalDelay never reverts + */ + function setWithdrawalDelayPostconditions( + bool success, + bytes memory returnData, + address[] memory actorsToUpdate, + address tokenToUpdate + ) internal { + if (success) { + _after(actorsToUpdate, tokenToUpdate); + onSuccessInvariantsGeneral(returnData, tokenToUpdate); + onSuccessInvariantsRootERC20BridgeFlowRate(returnData); + } else { + invariant_RTREV_12(returnData); + onFailInvariantsGeneral(returnData); + onFailInvariantsRootERC20BridgeFlowRate(returnData); + } + } + + /** + * @notice Postconditions for the unpause function for the root bridge + * @custom:invariant RTREV-13: unpause for the root bridge never reverts + */ + function unpauseRootPostconditions( + bool success, + bytes memory returnData, + address[] memory actorsToUpdate, + address tokenToUpdate + ) internal { + if (success) { + _after(actorsToUpdate, tokenToUpdate); + onSuccessInvariantsGeneral(returnData, tokenToUpdate); + onSuccessInvariantsRootERC20BridgeFlowRate(returnData); + } else { + invariant_RTREV_13(returnData); + onFailInvariantsGeneral(returnData); + onFailInvariantsRootERC20BridgeFlowRate(returnData); + } + } + + /** + * @notice Postconditions for the updateImxCumulativeDepositLimit function for the root bridge + * @custom:invariant RTREV-14: updateImxCumulativeDepositLimit never reverts unexpectedly + */ + function updateImxCumulativeDepositLimitPostconditions( + bool success, + bytes memory returnData, + address[] memory actorsToUpdate, + address tokenToUpdate + ) internal { + if (success) { + _after(actorsToUpdate, tokenToUpdate); + onSuccessInvariantsGeneral(returnData, tokenToUpdate); + onSuccessInvariantsRootERC20BridgeFlowRate(returnData); + } else { + invariant_RTREV_14(returnData); + onFailInvariantsGeneral(returnData); + onFailInvariantsRootERC20BridgeFlowRate(returnData); + } + } + + /** + * @notice invariants to run after each successful transaction for the root bridge + * @custom:invariant FLRT-01: If imxCumulativeDepositLimit is not 0, the rootIMX token balance of + * the root bridge should always be less than or equal to imxCumulativeDepositLimit + * + * @custom:invariant BAL-01: The WETH balance of the root bridge should always be 0 + */ + function onSuccessInvariantsRootERC20BridgeFlowRate(bytes memory returnData) internal { + invariant_FLRT_01(); + invariant_BAL_01(); + } + + /** + * @notice invariants to run after each failed transaction for the root bridge + */ + function onFailInvariantsRootERC20BridgeFlowRate(bytes memory returnData) internal {} +} diff --git a/test/fuzzing/helper/preconditions/PreconditionsBase.sol b/test/fuzzing/helper/preconditions/PreconditionsBase.sol new file mode 100644 index 00000000..bd8798ef --- /dev/null +++ b/test/fuzzing/helper/preconditions/PreconditionsBase.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.0; + +import "../../util/FunctionCalls.sol"; +import "../BeforeAfter.sol"; + +/** + * @title PreconditionsBase + * @author 0xScourgedev + * @notice Contains the base for all preconditions + */ +abstract contract PreconditionsBase is FunctionCalls, BeforeAfter { + error ClampFail(string); + error MultiCallFail(string); + + /** + * @notice modifier to set the current actor to the sender + */ + modifier setCurrentActor() { + if (_setActor) { + currentActor = address(uint160(msg.sender) + 1); + } + _; + } +} diff --git a/test/fuzzing/helper/preconditions/PreconditionsChildERC20Bridge.sol b/test/fuzzing/helper/preconditions/PreconditionsChildERC20Bridge.sol new file mode 100644 index 00000000..a28e0360 --- /dev/null +++ b/test/fuzzing/helper/preconditions/PreconditionsChildERC20Bridge.sol @@ -0,0 +1,283 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.0; + +import "./PreconditionsBase.sol"; + +/** + * @title PreconditionsChildERC20Bridge + * @author 0xScourgedev + * @notice Contains all preconditions for ChildERC20Bridge + */ +abstract contract PreconditionsChildERC20Bridge is PreconditionsBase { + /////////////////////////////////////////////////////////////////////////////////////////////// + // STRUCTS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + struct OnMessageReceiveChildParams { + bytes data; + address rootToken; + uint256 amount; + address sender; + address receiver; + } + + struct WithdrawParams { + address childToken; + uint256 amount; + uint256 value; + } + + struct WithdrawETHParams { + uint256 amount; + uint256 value; + } + + struct WithdrawETHToParams { + address receiver; + uint256 amount; + uint256 value; + } + + struct WithdrawIMXParams { + uint256 amount; + uint256 value; + } + + struct WithdrawIMXToParams { + address receiver; + uint256 amount; + uint256 value; + } + + struct WithdrawToParams { + address childToken; + address receiver; + uint256 amount; + uint256 value; + } + + struct WithdrawWIMXParams { + uint256 amount; + uint256 value; + } + + struct WithdrawWIMXToParams { + address receiver; + uint256 amount; + uint256 value; + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // PRECONDITIONS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * @notice Preconditions for the onMessageReceive function for the child bridge + * @custom:precondition Set the root token to a valid root token address + * @custom:precondition If the root token is WETH, set the root token to NATIVE_ETH + * @custom:precondition Set the sender and receiver to valid user addresses + * @custom:precondition Have a chance for the selector to be an invalid signature + * @custom:precondition The deposit amount is clamped between 0 and the maximum ERC20 balance + * @custom:precondition All inputs are encoded into bytes + */ + function onMessageReceiveChildPreconditions( + bool isDeposit, + uint8 rootTokenSelector, + uint8 senderSelector, + uint8 receiverSelector, + uint256 amount + ) internal returns (OnMessageReceiveChildParams memory) { + address rootToken = rootTokens[rootTokenSelector % rootTokens.length]; + if (rootToken == wETH) { + rootToken = NATIVE_ETH; + } + if (rootToken == rootIMXToken) { + amount = fl.clamp(amount, 0, childERC20Bridge.balance + COVERAGE_GAP); + } else { + amount = fl.clamp(amount, 0, MAX_ERC20_BALANCE); + } + bytes32 selector = isDeposit ? DEPOSIT_SIG : NOTHING_SIG; + address sender = USERS[senderSelector % USERS.length]; + address receiver = USERS[receiverSelector % USERS.length]; + bytes memory data = abi.encode(selector, rootToken, sender, receiver, amount); + return OnMessageReceiveChildParams({ + data: data, + rootToken: rootToken, + amount: amount, + sender: sender, + receiver: receiver + }); + } + + /** + * @notice Preconditions for the pause function for the child bridge + * @custom:precondition The child bridge is not already paused + */ + function pauseChildPreconditions() internal { + if (ChildERC20Bridge(payable(childERC20Bridge)).paused()) { + revert ClampFail("Child bridge already paused"); + } + } + + /** + * @notice Preconditions for the unpause function for the child bridge + * @custom:precondition The child bridge is currently paused + */ + function unpauseChildPreconditions() internal { + if (!ChildERC20Bridge(payable(childERC20Bridge)).paused()) { + revert ClampFail("Child bridge already unpaused"); + } + } + + /** + * @notice Preconditions for the withdraw function for the child bridge + * @custom:precondition Give a small chance for the transaction to go through when the child bridge is paused + * @custom:precondition Select a valid child token to withdraw + * @custom:precondition The amount is clamped between 0 and the current actor's child token balance + * @custom:precondition The value is clamped between 0 and the current actor's native balance + */ + function withdrawPreconditions(uint8 childTokenSelector, uint256 amount, uint256 value) + internal + returns (WithdrawParams memory) + { + if (ChildERC20Bridge(payable(childERC20Bridge)).paused() && block.number % EXECUTE_WHEN_PAUSED_ODDS != 0) { + revert ClampFail("Child bridge already paused"); + } + address childToken = childTokens[childTokenSelector % childTokens.length]; + return WithdrawParams({ + childToken: childToken, + amount: fl.clamp(amount, 0, ChildERC20(childToken).balanceOf(address(currentActor))), + value: fl.clamp(value, 0, currentActor.balance) + }); + } + + /** + * @notice Preconditions for the withdrawETH function for the child bridge + * @custom:precondition Give a small chance for the transaction to go through when the child bridge is paused + * @custom:precondition The amount is clamped between 0 and the current actor's child ETH token balance + * @custom:precondition The value is clamped between 0 and the current actor's native balance + */ + function withdrawETHPreconditions(uint256 amount, uint256 value) internal returns (WithdrawETHParams memory) { + if (ChildERC20Bridge(payable(childERC20Bridge)).paused() && block.number % EXECUTE_WHEN_PAUSED_ODDS != 0) { + revert ClampFail("Child bridge already paused"); + } + return WithdrawETHParams({ + amount: fl.clamp(amount, 0, ChildERC20(childETHToken).balanceOf(address(currentActor))), + value: fl.clamp(value, 0, currentActor.balance) + }); + } + + /** + * @notice Preconditions for the withdrawETHTo function for the child bridge + * @custom:precondition Give a small chance for the transaction to go through when the child bridge is paused + * @custom:precondition Set the receiver to a valid user address + * @custom:precondition The amount is clamped between 0 and the current actor's child ETH token balance + * @custom:precondition The value is clamped between 0 and the current actor's native balance + */ + function withdrawETHToPreconditions(uint8 receiverSelector, uint256 amount, uint256 value) + internal + returns (WithdrawETHToParams memory) + { + if (ChildERC20Bridge(payable(childERC20Bridge)).paused() && block.number % EXECUTE_WHEN_PAUSED_ODDS != 0) { + revert ClampFail("Child bridge already paused"); + } + value = fl.clamp(value, 0, currentActor.balance); + amount = fl.clamp(amount, 0, ChildERC20(childETHToken).balanceOf(address(currentActor))); + return WithdrawETHToParams({receiver: USERS[receiverSelector % USERS.length], amount: amount, value: value}); + } + + /** + * @notice Preconditions for the withdrawIMX function for the child bridge + * @custom:precondition Give a small chance for the transaction to go through when the child bridge is paused + * @custom:precondition The value is clamped between 0 and the current actor's native balance + * @custom:precondition The amount is clamped between 0 and the clamped transaction value plus a small gap for coverage purposes + */ + function withdrawIMXPreconditions(uint256 amount, uint256 value) internal returns (WithdrawIMXParams memory) { + if (ChildERC20Bridge(payable(childERC20Bridge)).paused() && block.number % EXECUTE_WHEN_PAUSED_ODDS != 0) { + revert ClampFail("Child bridge already paused"); + } + value = fl.clamp(value, 0, currentActor.balance); + amount = fl.clamp(amount, 0, value + COVERAGE_GAP); + return WithdrawIMXParams({amount: amount, value: value}); + } + + /** + * @notice Preconditions for the withdrawIMXTo function for the child bridge + * @custom:precondition Give a small chance for the transaction to go through when the child bridge is paused + * @custom:precondition Set the receiver to a valid user address + * @custom:precondition The value is clamped between 0 and the current actor's native balance + * @custom:precondition The amount is clamped between 0 and the clamped transaction value plus a small gap for coverage purposes + */ + function withdrawIMXToPreconditions(uint8 receiverSelector, uint256 amount, uint256 value) + internal + returns (WithdrawIMXToParams memory) + { + if (ChildERC20Bridge(payable(childERC20Bridge)).paused() && block.number % EXECUTE_WHEN_PAUSED_ODDS != 0) { + revert ClampFail("Child bridge already paused"); + } + value = fl.clamp(value, 0, currentActor.balance); + amount = fl.clamp(amount, 0, value + COVERAGE_GAP); + return WithdrawIMXToParams({receiver: USERS[receiverSelector % USERS.length], amount: amount, value: value}); + } + + /** + * @notice Preconditions for the withdrawTo function for the child bridge + * @custom:precondition Give a small chance for the transaction to go through when the child bridge is paused + * @custom:precondition select a valid child token to withdraw + * @custom:precondition Set the receiver to a valid user address + * @custom:precondition The amount is clamped between 0 and the current actor's child token balance + * @custom:precondition The value is clamped between 0 and the current actor's native balance + */ + function withdrawToPreconditions(uint8 childTokenSelector, uint8 receiverSelector, uint256 amount, uint256 value) + internal + returns (WithdrawToParams memory) + { + if (ChildERC20Bridge(payable(childERC20Bridge)).paused() && block.number % EXECUTE_WHEN_PAUSED_ODDS != 0) { + revert ClampFail("Child bridge already paused"); + } + address childToken = childTokens[childTokenSelector % childTokens.length]; + return WithdrawToParams({ + childToken: childToken, + receiver: USERS[receiverSelector % USERS.length], + amount: fl.clamp(amount, 0, ChildERC20(childToken).balanceOf(address(currentActor))), + value: fl.clamp(value, 0, currentActor.balance) + }); + } + + /** + * @notice Preconditions for the withdrawWIMX function for the child bridge + * @custom:precondition Give a small chance for the transaction to go through when the child bridge is paused + * @custom:precondition The amount is clamped between 0 and the current actor's WIMX token balance + * @custom:precondition The value is clamped between 0 and the current actor's native balance + */ + function withdrawWIMXPreconditions(uint256 amount, uint256 value) internal returns (WithdrawWIMXParams memory) { + if (ChildERC20Bridge(payable(childERC20Bridge)).paused() && block.number % EXECUTE_WHEN_PAUSED_ODDS != 0) { + revert ClampFail("Child bridge already paused"); + } + return WithdrawWIMXParams({ + amount: fl.clamp(amount, 0, ChildERC20(wIMX).balanceOf(address(currentActor))), + value: fl.clamp(value, 0, currentActor.balance) + }); + } + + /** + * @notice Preconditions for the withdrawWIMXTo function for the child bridge + * @custom:precondition Give a small chance for the transaction to go through when the child bridge is paused + * @custom:precondition Set the receiver to a valid user address + * @custom:precondition The amount is clamped between 0 and the current actor's WIMX token balance + * @custom:precondition The value is clamped between 0 and the current actor's native balance + */ + function withdrawWIMXToPreconditions(uint8 receiverSelector, uint256 amount, uint256 value) + internal + returns (WithdrawWIMXToParams memory) + { + if (ChildERC20Bridge(payable(childERC20Bridge)).paused() && block.number % EXECUTE_WHEN_PAUSED_ODDS != 0) { + revert ClampFail("Child bridge already paused"); + } + return WithdrawWIMXToParams({ + receiver: USERS[receiverSelector % USERS.length], + amount: fl.clamp(amount, 0, ChildERC20(wIMX).balanceOf(address(currentActor))), + value: fl.clamp(value, 0, currentActor.balance) + }); + } +} diff --git a/test/fuzzing/helper/preconditions/PreconditionsRootERC20BridgeFlowRate.sol b/test/fuzzing/helper/preconditions/PreconditionsRootERC20BridgeFlowRate.sol new file mode 100644 index 00000000..690b71db --- /dev/null +++ b/test/fuzzing/helper/preconditions/PreconditionsRootERC20BridgeFlowRate.sol @@ -0,0 +1,415 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.0; + +import "./PreconditionsBase.sol"; + +/** + * @title PreconditionsRootERC20BridgeFlowRate + * @author 0xScourgedev + * @notice Contains all preconditions for RootERC20BridgeFlowRate + */ +abstract contract PreconditionsRootERC20BridgeFlowRate is PreconditionsBase { + /////////////////////////////////////////////////////////////////////////////////////////////// + // STRUCTS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + struct DepositParams { + address rootToken; + uint256 amount; + uint256 value; + } + + struct DepositETHParams { + uint256 amount; + uint256 value; + } + + struct DepositToParams { + address rootToken; + address receiver; + uint256 amount; + uint256 value; + } + + struct DepositToETHParams { + address receiver; + uint256 amount; + uint256 value; + } + + struct FinaliseQueuedWithdrawalParams { + address receiver; + uint256 index; + } + + struct FinaliseQueuedWithdrawalsAggregatedParams { + address receiver; + address token; + uint256[] indices; + } + + struct OnMessageReceiveRootParams { + bytes data; + address rootToken; + address receiver; + uint256 amount; + } + + struct SetRateControlThresholdParams { + address token; + uint256 capacity; + uint256 refillRate; + uint256 largeTransferThreshold; + } + + struct SetWithdrawalDelayParams { + uint256 delay; + } + + struct UpdateImxCumulativeDepositLimitParams { + uint256 newImxCumulativeDepositLimit; + } + + struct UpdateRootBridgeAdaptorParams { + address newRootBridgeAdaptor; + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // PRECONDITIONS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * @notice Preconditions for the activateWithdrawalQueue function for the root bridge + * @custom:precondition The withdrawal queue is not already activated + */ + function activateWithdrawalQueuePreconditions() internal { + if (RootERC20BridgeFlowRate(payable(rootERC20BridgeFlowRate)).withdrawalQueueActivated()) { + revert ClampFail("Withdrawal queue already activated"); + } + } + + /** + * @notice Preconditions for the deactivateWithdrawalQueue function for the root bridge + * @custom:precondition The withdrawal queue is currently activated + */ + function deactivateWithdrawalQueuePreconditions() internal { + if (!RootERC20BridgeFlowRate(payable(rootERC20BridgeFlowRate)).withdrawalQueueActivated()) { + revert ClampFail("Withdrawal queue already deactivated"); + } + } + + /** + * @notice Preconditions for the deposit function for the root bridge + * @custom:precondition Give a small chance for the transaction to go through when the root bridge is paused + * @custom:precondition Select a valid root token to deposit + * @custom:precondition The amount is clamped between 0 and the current actor's root token balance + * @custom:precondition The value is clamped between 0 and the current actor's native balance + */ + function depositPreconditions(uint8 rootTokenSelector, uint256 amount, uint256 value) + internal + returns (DepositParams memory) + { + if ( + RootERC20BridgeFlowRate(payable(rootERC20BridgeFlowRate)).paused() + && block.number % EXECUTE_WHEN_PAUSED_ODDS != 0 + ) { + revert ClampFail("Root bridge is paused"); + } + address rootToken = rootTokens[rootTokenSelector % rootTokens.length]; + return DepositParams({ + rootToken: rootToken, + amount: fl.clamp(amount, 0, ChildERC20(rootToken).balanceOf(currentActor)), + value: fl.clamp(value, 0, currentActor.balance) + }); + } + + /** + * @notice Preconditions for the depositETH function for the root bridge + * @custom:precondition Give a small chance for the transaction to go through when the root bridge is paused + * @custom:precondition The value is clamped between 0 and the current actor's native balance + * @custom:precondition The amount is clamped between 0 and the value plus a small gap for coverage purposes + */ + function depositETHPreconditions(uint256 amount, uint256 value) internal returns (DepositETHParams memory) { + if ( + RootERC20BridgeFlowRate(payable(rootERC20BridgeFlowRate)).paused() + && block.number % EXECUTE_WHEN_PAUSED_ODDS != 0 + ) { + revert ClampFail("Root bridge is paused"); + } + value = fl.clamp(value, 0, currentActor.balance); + amount = fl.clamp(amount, 0, value + COVERAGE_GAP); + return DepositETHParams({amount: amount, value: value}); + } + + /** + * @notice Preconditions for the depositTo function for the root bridge + * @custom:precondition Give a small chance for the transaction to go through when the root bridge is paused + * @custom:precondition Select a valid root token to deposit + * @custom:precondition Set the receiver to a valid user address + * @custom:precondition The amount is clamped between 0 and the current actor's root token balance + * @custom:precondition The value is clamped between 0 and the current actor's native balance + */ + function depositToPreconditions(uint8 rootTokenSelector, uint8 receiverSelector, uint256 amount, uint256 value) + internal + returns (DepositToParams memory) + { + if ( + RootERC20BridgeFlowRate(payable(rootERC20BridgeFlowRate)).paused() + && block.number % EXECUTE_WHEN_PAUSED_ODDS != 0 + ) { + revert ClampFail("Root bridge is paused"); + } + address rootToken = rootTokens[rootTokenSelector % rootTokens.length]; + return DepositToParams({ + rootToken: rootToken, + receiver: USERS[receiverSelector % USERS.length], + amount: fl.clamp(amount, 0, ChildERC20(rootToken).balanceOf(currentActor)), + value: fl.clamp(value, 0, currentActor.balance) + }); + } + + /** + * @notice Preconditions for the depositToETH function for the root bridge + * @custom:precondition Give a small chance for the transaction to go through when the root bridge is paused + * @custom:precondition Set the receiver to a valid user address + * @custom:precondition The value is clamped between 0 and the current actor's native balance + * @custom:precondition The amount is clamped between 0 and the value plus a small gap for coverage purposes + */ + function depositToETHPreconditions(uint8 receiverSelector, uint256 amount, uint256 value) + internal + returns (DepositToETHParams memory) + { + if ( + RootERC20BridgeFlowRate(payable(rootERC20BridgeFlowRate)).paused() + && block.number % EXECUTE_WHEN_PAUSED_ODDS != 0 + ) { + revert ClampFail("Root bridge is paused"); + } + value = fl.clamp(value, 0, currentActor.balance); + amount = fl.clamp(amount, 0, value + COVERAGE_GAP); + return DepositToETHParams({receiver: USERS[receiverSelector % USERS.length], amount: amount, value: value}); + } + + /** + * @notice Preconditions for the finaliseQueuedWithdrawal function for the root bridge + * @custom:precondition Give a small chance for the transaction to go through when the root bridge is paused + * @custom:precondition Set the receiver to a valid user address + * @custom:precondition The index is clamped between 0 and the pending withdrawals length plus 1 for coverage purposes + */ + function finaliseQueuedWithdrawalPreconditions(uint8 receiverSelector, uint256 index) + internal + returns (FinaliseQueuedWithdrawalParams memory) + { + if ( + RootERC20BridgeFlowRate(payable(rootERC20BridgeFlowRate)).paused() + && block.number % EXECUTE_WHEN_PAUSED_ODDS != 0 + ) { + revert ClampFail("Root bridge is paused"); + } + address receiver = USERS[receiverSelector % USERS.length]; + index = fl.clamp( + index, + 0, + FlowRateWithdrawalQueue(payable(rootERC20BridgeFlowRate)).getPendingWithdrawalsLength(receiver) + 1 + ); + return FinaliseQueuedWithdrawalParams({receiver: receiver, index: index}); + } + + /** + * @notice Preconditions for the finaliseQueuedWithdrawalsAggregated function for the root bridge + * @custom:precondition Give a small chance for the transaction to go through when the root bridge is paused + * @custom:precondition Set the receiver to a valid user address + * @custom:precondition Select a valid root token to finalize the withdrawal for + * @custom:precondition The length is clamped between 0 and the pending withdrawals length for the + * receiver plus 1 for coverage purposes + * + * @custom:precondition Indices values are generated from the entropy value + */ + function finaliseQueuedWithdrawalsAggregatedPreconditions( + uint8 receiverSelector, + uint8 tokenSelector, + uint256 length, + uint256 entropy + ) internal returns (FinaliseQueuedWithdrawalsAggregatedParams memory) { + if ( + RootERC20BridgeFlowRate(payable(rootERC20BridgeFlowRate)).paused() + && block.number % EXECUTE_WHEN_PAUSED_ODDS != 0 + ) { + revert ClampFail("Root bridge is paused"); + } + address receiver = USERS[receiverSelector % USERS.length]; + address token = rootTokens[tokenSelector % rootTokens.length]; + uint256 maxLength = + FlowRateWithdrawalQueue(payable(rootERC20BridgeFlowRate)).getPendingWithdrawalsLength(receiver) + 1; + length = fl.clamp(length, 0, maxLength); + return FinaliseQueuedWithdrawalsAggregatedParams({ + receiver: receiver, + token: token, + indices: getIndicesFromEntropy(entropy, length, maxLength) + }); + } + + /** + * @notice Preconditions for the onMessageReceive function for the root bridge + * @custom:precondition Give a small chance for the transaction to go through when the root bridge is paused + * @custom:precondition Set the receiver to a valid user address + * @custom:precondition Set the withdrawer to a valid user address + * @custom:precondition Select a valid root token to finalize the withdrawal for + * @custom:precondition The amount is clamped between 0 and the root bridge balance of the selected token + * minus the queued amounts + * + * @custom:precondition The amount of pending withdrawals cannot exceed the maximum in queue constant + * @custom:precondition Have a chance for the selector to be an invalid signature + * @custom:precondition All inputs are encoded into bytes + */ + function onMessageReceiveRootPreconditions( + bool isWithdraw, + uint8 rootTokenSelector, + uint8 withdrawerSelector, + uint8 receiverSelector, + uint256 amount + ) internal returns (OnMessageReceiveRootParams memory) { + if ( + RootERC20BridgeFlowRate(payable(rootERC20BridgeFlowRate)).paused() + && block.number % EXECUTE_WHEN_PAUSED_ODDS != 0 + ) { + revert ClampFail("Root bridge is paused"); + } + rootTokens.push(NATIVE_ETH); + address receiver = USERS[receiverSelector % USERS.length]; + address rootToken = rootTokens[rootTokenSelector % rootTokens.length]; + uint256 rootBridgeBalance = 0; + if (rootToken == NATIVE_ETH) { + rootBridgeBalance += rootERC20BridgeFlowRate.balance; + } else { + rootBridgeBalance += ChildERC20(rootToken).balanceOf(rootERC20BridgeFlowRate); + } + + { + uint256 rootBridgeQueuedAmounts = 0; + + for (uint256 i = 0; i < USERS.length; i++) { + uint256 pendingLength = + FlowRateWithdrawalQueue(payable(rootERC20BridgeFlowRate)).getPendingWithdrawalsLength(USERS[i]); + + if (pendingLength > MAX_IN_QUEUE) { + revert ClampFail("Too many pending withdrawals"); + } + + FlowRateWithdrawalQueue.FindPendingWithdrawal[] memory pending = FlowRateWithdrawalQueue( + payable(rootERC20BridgeFlowRate) + ).findPendingWithdrawals(USERS[i], rootToken, 0, pendingLength, MAX_IN_QUEUE); + for (uint256 j = 0; j < pending.length; j++) { + rootBridgeQueuedAmounts += pending[j].amount; + } + } + + amount = fl.clamp(amount, 0, rootBridgeBalance - rootBridgeQueuedAmounts); + } + bytes32 selector = isWithdraw ? WITHDRAW_SIG : NOTHING_SIG; + bytes memory data = abi.encode( + selector, + rootTokens[rootTokenSelector % rootTokens.length], + USERS[withdrawerSelector % USERS.length], + receiver, + amount + ); + + rootTokens.pop(); + return OnMessageReceiveRootParams({data: data, rootToken: rootToken, receiver: receiver, amount: amount}); + } + + /** + * @notice Preconditions for the pause function for the root bridge + * @custom:precondition The root bridge is not already paused + */ + function pauseRootPreconditions() internal { + if (RootERC20BridgeFlowRate(payable(rootERC20BridgeFlowRate)).paused()) { + revert ClampFail("Root bridge already paused"); + } + } + + /** + * @notice Preconditions for the setRateControlThreshold function for the root bridge + * @custom:precondition Select a valid root token to set the rate control thresholds for + * @custom:precondition The capacity is clamped between 0 and the total supply of the token + * @custom:precondition The refill rate is clamped between 0 and the capacity plus a small gap for coverage purposes + * @custom:precondition The large transfer threshold is clamped between 0 and the total supply + * of the token plus a small gap for coverage purposes + */ + function setRateControlThresholdPreconditions( + uint8 tokenSelector, + uint256 capacity, + uint256 refillRate, + uint256 largeTransferThreshold + ) internal returns (SetRateControlThresholdParams memory) { + address token = rootTokens[tokenSelector % rootTokens.length]; + capacity = fl.clamp(capacity, 0, ChildERC20(token).totalSupply()); + refillRate = fl.clamp(refillRate, 0, capacity + MAX_REFILL_RATE_GAP); + largeTransferThreshold = + fl.clamp(largeTransferThreshold, 0, ChildERC20(token).totalSupply() + MAX_REFILL_RATE_GAP); + return SetRateControlThresholdParams({ + token: token, + capacity: capacity, + refillRate: refillRate, + largeTransferThreshold: largeTransferThreshold + }); + } + + /** + * @notice Preconditions for the setWithdrawalDelay function for the root bridge + * @custom:precondition The delay is clamped between 0 and the maximum withdrawal delay + */ + function setWithdrawalDelayPreconditions(uint256 delay) internal returns (SetWithdrawalDelayParams memory) { + return SetWithdrawalDelayParams({delay: fl.clamp(delay, 0, MAX_WITHDRAWAL_DELAY)}); + } + + /** + * @notice Preconditions for the unpause function for the root bridge + * @custom:precondition The root bridge is not currently unpaused + */ + function unpauseRootPreconditions() internal { + if (!RootERC20BridgeFlowRate(payable(rootERC20BridgeFlowRate)).paused()) { + revert ClampFail("Root bridge already unpaused"); + } + } + + /** + * @notice Preconditions for the updateImxCumulativeDepositLimit function for the root bridge + * @custom:precondition The new IMX cumulative deposit limit is clamped between 0 and the + * maximum ERC20 balance multiplied by the root IMX token decimals + */ + function updateImxCumulativeDepositLimitPreconditions(uint256 newImxCumulativeDepositLimit) + internal + returns (UpdateImxCumulativeDepositLimitParams memory) + { + return UpdateImxCumulativeDepositLimitParams({ + newImxCumulativeDepositLimit: fl.clamp( + newImxCumulativeDepositLimit, 0, MAX_ERC20_BALANCE * ChildERC20(rootIMXToken).decimals() + ) + }); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // UTILS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * @notice This function gets an array of indices from an entropy value + * @param entropy The entropy to generate the indices from + * @param length The length of the indices array + * @param maxLength The maximum value of each of the indices + */ + function getIndicesFromEntropy(uint256 entropy, uint256 length, uint256 maxLength) + internal + returns (uint256[] memory) + { + uint256[] memory indices = new uint256[](length); + for (uint256 i = 0; i < length; i++) { + uint256 index = uint256(keccak256(abi.encode(entropy, i))); + index = fl.clamp(index, 0, maxLength); + indices[i] = index; + } + return indices; + } +} diff --git a/test/fuzzing/mocks/MockAdaptor.sol b/test/fuzzing/mocks/MockAdaptor.sol new file mode 100644 index 00000000..1e63401f --- /dev/null +++ b/test/fuzzing/mocks/MockAdaptor.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: Apache 2.0 +pragma solidity 0.8.19; + +import {IChildBridgeAdaptor} from "../../../src/interfaces/child/IChildBridgeAdaptor.sol"; +import {IRootBridgeAdaptor} from "../../../src/interfaces/root/IRootBridgeAdaptor.sol"; + +import {LibLog} from "@perimetersec/fuzzlib/src/libraries/LibLog.sol"; + +interface MessageReceiver { + function onMessageReceive(bytes calldata data) external; +} + +contract MockAdaptor is IChildBridgeAdaptor, IRootBridgeAdaptor { + uint256 otherChainId; + MessageReceiver bridge; + + constructor() {} + + function initialize(address _bridge) public { + bridge = MessageReceiver(_bridge); + } + + function sendMessage(bytes calldata payload, address /*refundRecipient*/ ) + external + payable + override(IChildBridgeAdaptor, IRootBridgeAdaptor) + {} + + function onMessageReceive(bytes calldata data) public { + bridge.onMessageReceive(data); + } +} diff --git a/test/fuzzing/properties/Properties.sol b/test/fuzzing/properties/Properties.sol new file mode 100644 index 00000000..ef0bc406 --- /dev/null +++ b/test/fuzzing/properties/Properties.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.0; + +import "./Properties_RTREV.sol"; +import "./Properties_FLRT.sol"; +import "./Properties_PAUSE.sol"; +import "./Properties_CLDREV.sol"; +import "./Properties_SPLY.sol"; +import "./Properties_BAL.sol"; + +/** + * @title Properties + * @author 0xScourgedev + * @notice Composite contract for all of the properties, and contains general invariants + */ +abstract contract Properties is + Properties_RTREV, + Properties_FLRT, + Properties_PAUSE, + Properties_CLDREV, + Properties_SPLY, + Properties_BAL +{} diff --git a/test/fuzzing/properties/PropertiesBase.sol b/test/fuzzing/properties/PropertiesBase.sol new file mode 100644 index 00000000..3a46f16d --- /dev/null +++ b/test/fuzzing/properties/PropertiesBase.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.0; + +import "@perimetersec/fuzzlib/src/FuzzBase.sol"; + +import "./PropertiesDescriptions.sol"; +import "../helper/BeforeAfter.sol"; + +/** + * @title PropertiesBase + * @author 0xScourgedev + * @notice Composite contract for all of the dependencies of the properties + */ +abstract contract PropertiesBase is FuzzBase, BeforeAfter, PropertiesDescriptions {} diff --git a/test/fuzzing/properties/PropertiesDescriptions.sol b/test/fuzzing/properties/PropertiesDescriptions.sol new file mode 100644 index 00000000..734eb4c4 --- /dev/null +++ b/test/fuzzing/properties/PropertiesDescriptions.sol @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.0; + +/** + * @title PropertiesDescriptions + * @author 0xScourgedev + * @notice Descriptions strings for the invariants + */ +abstract contract PropertiesDescriptions { + string internal constant FLRT_01 = + "FLRT-01: If imxCumulativeDepositLimit is not 0, the rootIMX token balance of the root bridge should always be less than or equal to imxCumulativeDepositLimit"; + string internal constant FLRT_02 = + "FLRT-02: If the withdrawal amount is greater than the largeTransferThreshold, the withdrawal must be added to the user's withdrawal queue"; + string internal constant FLRT_03 = + "FLRT-03: If the bucket capacity for a token is 0, then any withdrawal of that token must be added to the user's withdrawal queue"; + string internal constant FLRT_04 = + "FLRT-04: If withdrawalQueueActivated is true, then any withdrawal of any token must be added to the user's withdrawal queue"; + string internal constant FLRT_05 = + "FLRT-05: If finaliseQueuedWithdrawal is successfully called, then the queued withdrawal initiated timestamp plus the withdrawal delay must be less than or equal to the current block timestamp"; + string internal constant FLRT_06 = + "FLRT-06: After a successful withdrawal of a non-zero amount on the root bridge, if the bucket capacity is not zero, then the token bucket depth is never equal to the bucket capacity"; + string internal constant FLRT_07 = + "FLRT-07: After a successful withdraw on the root bridge, if the withdrawn amount is less than the minimum of the refill rate multiplied by the time elapsed since last withdrawal and the bucket capacity minus the prior real bucket depth, then the bucket depth strictly increases"; + string internal constant FLRT_08 = + "FLRT-08: After a successful withdraw on the root bridge, the bucket depth is always less than or equal to the bucket capacity"; + string internal constant FLRT_09 = + "FLRT-09: When the withdrawalQueue is not activated, after a successful withdrawal of an amount less than the largeTransferThreshold on the root bridge and a withdrawal was added to the user's withdrawal queue, the bucket depth is always 0"; + string internal constant FLRT_10 = + "FLRT-10: When the withdrawalQueue is not activated, after a successful withdrawal of an amount less than the largeTransferThreshold on the root bridge, if the withdrawn amount is greater or equal to minimum of the refill rate multiplied by the time elapsed since last withdrawal plus the prior real bucket depth and the bucket capacity, then the withdrawal must be added to the user's withdrawal queue"; + string internal constant FLRT_11 = + "FLRT-11: If the depth of the token bucket is non-zero before a successful withdrawal on the root bridge and the depth of the token bucket is zero after the withdrawal, then the withdrawal queue must be activated"; + string internal constant FLRT_12 = + "FLRT-12: After each successful withdrawal on the root bridge for a token with a non-zero capacity, the bucket refillTime is always updated to the current block timestamp"; + + string internal constant SPLY_01 = + "SPLY-01: The sum of the token balances of each user and the bridges should be exactly equal to the token total supply"; + string internal constant SPLY_02 = + "SPLY-02: The total supply of root WETH must be decreased by the amount of WETH deposited by the user"; + string internal constant SPLY_03 = + "SPLY-03: The total supply of child ERC20s must be increased by exactly the amount of tokens deposited by the user"; + string internal constant SPLY_04 = + "SPLY-04: The total supply of child ERC20s must be decreased by exactly the amount of tokens withdrawn by the user"; + + string internal constant BAL_01 = "BAL-01: The WETH balance of the root bridge should always be 0"; + string internal constant BAL_02 = "BAL-02: The WIMX balance of the child bridge should always be 0"; + string internal constant BAL_03 = + "BAL-03: The native balance of the root adaptor increases by exactly the gas fees paid by the users"; + string internal constant BAL_04 = + "BAL-04: The native balance of the child adaptor increases by exactly the gas fees paid by the users"; + string internal constant BAL_05 = + "BAL-05: The token balance, excluding WETH, of the root bridge increases by exactly the amount of tokens deposited by the user"; + string internal constant BAL_06 = + "BAL-06: The native balance of the root bridge increases by exactly the amount of WETH deposited by the user"; + string internal constant BAL_07 = + "BAL-07: When depositing ETH, the native balance of the root bridge increases by exactly the amount deposited by the user, minus the gas fees"; + string internal constant BAL_08 = + "BAL-08: When withdrawing IMX, the native balance of the child bridge increases by exactly the amount withdrawn by the user, minus the gas fees"; + string internal constant BAL_09 = + "BAL-09: The native balance of the child bridge decreases by exactly the amount of root ERC20 IMX deposited by the user"; + + string internal constant RTREV_01 = "RTREV-01: activateWithdrawalQueue never reverts"; + string internal constant RTREV_02 = "RTREV-02: deactivateWithdrawalQueue never reverts"; + string internal constant RTREV_03 = "RTREV-03: deposit never reverts unexpectedly"; + string internal constant RTREV_04 = "RTREV-04: depositETH never reverts unexpectedly"; + string internal constant RTREV_05 = "RTREV-05: depositTo never reverts unexpectedly"; + string internal constant RTREV_06 = "RTREV-06: depositToETH never reverts unexpectedly"; + string internal constant RTREV_07 = "RTREV-07: finaliseQueuedWithdrawal never reverts unexpectedly"; + string internal constant RTREV_08 = "RTREV-08: finaliseQueuedWithdrawalAggregated never reverts unexpectedly"; + string internal constant RTREV_09 = "RTREV-09: onMessageReceive for the root bridge never reverts unexpectedly"; + string internal constant RTREV_10 = "RTREV-10: pause for the root bridge never reverts"; + string internal constant RTREV_11 = "RTREV-11: setRateControlThreshold never reverts"; + string internal constant RTREV_12 = "RTREV-12: setWithdrawalDelay never reverts"; + string internal constant RTREV_13 = "RTREV-13: unpause for the root bridge never reverts"; + string internal constant RTREV_14 = "RTREV-14: updateImxCumulativeDepositLimit never reverts unexpectedly"; + + string internal constant CLDREV_01 = "CLDREV-01: onMessageReceive for the child bridge never reverts unexpectedly"; + string internal constant CLDREV_02 = "CLDREV-02: pause for the child bridge never reverts"; + string internal constant CLDREV_03 = "CLDREV-03: unpause for the child bridge never reverts"; + string internal constant CLDREV_04 = "CLDREV-04: withdraw never reverts unexpectedly"; + string internal constant CLDREV_05 = "CLDREV-05: withdrawETH never reverts unexpectedly"; + string internal constant CLDREV_06 = "CLDREV-06: withdrawETHTo never reverts unexpectedly"; + string internal constant CLDREV_07 = "CLDREV-07: withdrawIMX never reverts unexpectedly"; + string internal constant CLDREV_08 = "CLDREV-08: withdrawIMXTo never reverts unexpectedly"; + string internal constant CLDREV_09 = "CLDREV-09: withdrawTo never reverts unexpectedly"; + string internal constant CLDREV_10 = "CLDREV-10: withdrawWIMX never reverts unexpectedly"; + string internal constant CLDREV_11 = "CLDREV-11: withdrawWIMXTo never reverts unexpectedly"; + + string internal constant PAUSE_01 = "PAUSE-01: deposit always reverts when the root bridge is paused"; + string internal constant PAUSE_02 = "PAUSE-02: depositETH always reverts when the root bridge is paused"; + string internal constant PAUSE_03 = "PAUSE-03: depositTo always reverts when the root bridge is paused"; + string internal constant PAUSE_04 = "PAUSE-04: depositToETH always reverts when the root bridge is paused"; + string internal constant PAUSE_05 = + "PAUSE-05: finaliseQueuedWithdrawal always reverts when the root bridge is paused"; + string internal constant PAUSE_06 = + "PAUSE-06: finaliseQueuedWithdrawalAggregated always reverts when the root bridge is paused"; + string internal constant PAUSE_07 = + "PAUSE-07: onMessageReceive for the root bridge always reverts when the root bridge is paused"; + string internal constant PAUSE_08 = "PAUSE-08: withdraw always reverts when the child bridge is paused"; + string internal constant PAUSE_09 = "PAUSE-09: withdrawETH always reverts when the child bridge is paused"; + string internal constant PAUSE_10 = "PAUSE-10: withdrawETHTo always reverts when the child bridge is paused"; + string internal constant PAUSE_11 = "PAUSE-11: withdrawIMX always reverts when the child bridge is paused"; + string internal constant PAUSE_12 = "PAUSE-12: withdrawIMXTo always reverts when the child bridge is paused"; + string internal constant PAUSE_13 = "PAUSE-13: withdrawTo always reverts when the child bridge is paused"; + string internal constant PAUSE_14 = "PAUSE-14: withdrawWIMX always reverts when the child bridge is paused"; + string internal constant PAUSE_15 = "PAUSE-15: withdrawWIMXTo always reverts when the child bridge is paused"; + string internal constant PAUSE_16 = + "PAUSE-16: onMessageReceive for the child bridge always reverts when the child bridge is paused"; +} diff --git a/test/fuzzing/properties/Properties_BAL.sol b/test/fuzzing/properties/Properties_BAL.sol new file mode 100644 index 00000000..3e16d401 --- /dev/null +++ b/test/fuzzing/properties/Properties_BAL.sol @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.0; + +import "./PropertiesBase.sol"; + +/** + * @title Properties_BAL + * @author 0xScourgedev + * @notice Contains all BAL invariants + */ +abstract contract Properties_BAL is PropertiesBase { + /////////////////////////////////////////////////////////////////////////////////////////////// + // INVARIANTS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * @custom:invariant BAL-01: The WETH balance of the root bridge should always be 0 + */ + function invariant_BAL_01() internal { + fl.eq(states[1].rootWETHBalOfRootBridge, 0, BAL_01); + } + + /** + * @custom:invariant BAL-02: The WIMX balance of the child bridge should always be 0 + */ + function invariant_BAL_02() internal { + fl.eq(states[1].childWIMXBalOfChildBridge, 0, BAL_02); + } + + /** + * @custom:invariant BAL-03: The native balance of the root adaptor increases by exactly the + * gas fees paid by the users + */ + function invariant_BAL_03(uint256 gas) internal { + fl.eq(states[1].nativeBalanceOfRootAdaptor, states[0].nativeBalanceOfRootAdaptor + gas, BAL_03); + } + + /** + * @custom:invariant BAL-04: The native balance of the child adaptor increases by exactly + * the gas fees paid by the users + */ + function invariant_BAL_04(uint256 gas) internal { + fl.eq(states[1].nativeBalanceOfChildAdaptor, states[0].nativeBalanceOfChildAdaptor + gas, BAL_04); + } + + /** + * @custom:invariant BAL-05: The token balance, excluding WETH, of the root bridge increases + * by exactly the amount of tokens deposited by the user + */ + function invariant_BAL_05(address token, uint256 amount) internal { + if (token == wETH) return; + fl.eq( + states[1].tokenStates[token].balances[rootERC20BridgeFlowRate], + states[0].tokenStates[token].balances[rootERC20BridgeFlowRate] + amount, + BAL_05 + ); + } + + /** + * @custom:invariant BAL-06: The native balance of the root bridge increases by exactly the + * amount of WETH deposited by the user + */ + function invariant_BAL_06(address token, uint256 amount) internal { + if (token != wETH) return; + fl.eq(states[1].nativeBalanceOfRootBridge, states[0].nativeBalanceOfRootBridge + amount, BAL_06); + } + + /** + * @custom:invariant BAL-07: When depositing ETH, the native balance of the root bridge + * increases by exactly the amount deposited by the user, minus the gas fees + */ + function invariant_BAL_07(uint256 amount) internal { + fl.eq(states[1].nativeBalanceOfRootBridge, states[0].nativeBalanceOfRootBridge + amount, BAL_07); + } + + /** + * @custom:invariant BAL-08: When withdrawing IMX, the native balance of the child bridge + * increases by exactly the amount withdrawn by the user, minus the gas fees + */ + function invariant_BAL_08(uint256 amount) internal { + fl.eq(states[1].nativeBalanceOfChildBridge, states[0].nativeBalanceOfChildBridge + amount, BAL_08); + } + + /** + * @custom:invariant BAL-09: The native balance of the child bridge decreases by exactly + * the amount of root ERC20 IMX deposited by the user + */ + function invariant_BAL_09(address token, uint256 amount) internal { + if (token != NATIVE_IMX && token != address(0)) return; + fl.eq(states[1].nativeBalanceOfChildBridge, states[0].nativeBalanceOfChildBridge - amount, BAL_09); + } +} diff --git a/test/fuzzing/properties/Properties_CLDREV.sol b/test/fuzzing/properties/Properties_CLDREV.sol new file mode 100644 index 00000000..ba329fc6 --- /dev/null +++ b/test/fuzzing/properties/Properties_CLDREV.sol @@ -0,0 +1,150 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.0; + +import "./PropertiesBase.sol"; + +/** + * @title Properties_CLDREV + * @author 0xScourgedev + * @notice Contains all CLDREV invariants + */ +abstract contract Properties_CLDREV is PropertiesBase { + /////////////////////////////////////////////////////////////////////////////////////////////// + // INVARIANTS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * @custom:invariant CLDREV-01: onMessageReceive for the child bridge never reverts unexpectedly + */ + function invariant_CLDREV_01(bytes memory returnData) internal { + if (states[1].childBridgePaused) { + return; + } + bytes4[] memory allowedErrors = new bytes4[](2); + allowedErrors[0] = IChildERC20BridgeErrors.InvalidData.selector; + allowedErrors[1] = IChildERC20BridgeErrors.InsufficientIMX.selector; + fl.errAllow(bytes4(returnData), allowedErrors, CLDREV_01); + } + + /** + * @custom:invariant CLDREV-02: pause for the child bridge never reverts + */ + function invariant_CLDREV_02(bytes memory returnData) internal { + bytes4[] memory allowedErrors = new bytes4[](0); + fl.errAllow(bytes4(returnData), allowedErrors, CLDREV_02); + } + + /** + * @custom:invariant CLDREV-03: unpause for the child bridge never reverts + */ + function invariant_CLDREV_03(bytes memory returnData) internal { + bytes4[] memory allowedErrors = new bytes4[](0); + fl.errAllow(bytes4(returnData), allowedErrors, CLDREV_03); + } + + /** + * @custom:invariant CLDREV-04: withdraw never reverts unexpectedly + */ + function invariant_CLDREV_04(bytes memory returnData) internal { + if (states[1].childBridgePaused) { + return; + } + bytes4[] memory allowedErrors = new bytes4[](2); + allowedErrors[0] = IChildERC20BridgeErrors.ZeroAmount.selector; + allowedErrors[1] = IChildERC20BridgeErrors.NoGas.selector; + fl.errAllow(bytes4(returnData), allowedErrors, CLDREV_04); + } + + /** + * @custom:invariant CLDREV-05: withdrawETH never reverts unexpectedly + */ + function invariant_CLDREV_05(bytes memory returnData) internal { + if (states[1].childBridgePaused) { + return; + } + bytes4[] memory allowedErrors = new bytes4[](2); + allowedErrors[0] = IChildERC20BridgeErrors.ZeroAmount.selector; + allowedErrors[1] = IChildERC20BridgeErrors.NoGas.selector; + fl.errAllow(bytes4(returnData), allowedErrors, CLDREV_05); + } + + /** + * @custom:invariant CLDREV-06: withdrawETHTo never reverts unexpectedly + */ + function invariant_CLDREV_06(bytes memory returnData) internal { + if (states[1].childBridgePaused) { + return; + } + bytes4[] memory allowedErrors = new bytes4[](2); + allowedErrors[0] = IChildERC20BridgeErrors.ZeroAmount.selector; + allowedErrors[1] = IChildERC20BridgeErrors.NoGas.selector; + fl.errAllow(bytes4(returnData), allowedErrors, CLDREV_06); + } + + /** + * @custom:invariant CLDREV-07: withdrawIMX never reverts unexpectedly + */ + function invariant_CLDREV_07(bytes memory returnData) internal { + if (states[1].childBridgePaused) { + return; + } + bytes4[] memory allowedErrors = new bytes4[](3); + allowedErrors[0] = IChildERC20BridgeErrors.ZeroAmount.selector; + allowedErrors[1] = IChildERC20BridgeErrors.NoGas.selector; + allowedErrors[2] = IChildERC20BridgeErrors.InsufficientValue.selector; + fl.errAllow(bytes4(returnData), allowedErrors, CLDREV_07); + } + + /** + * @custom:invariant CLDREV-08: withdrawIMXTo never reverts unexpectedly + */ + function invariant_CLDREV_08(bytes memory returnData) internal { + if (states[1].childBridgePaused) { + return; + } + bytes4[] memory allowedErrors = new bytes4[](3); + allowedErrors[0] = IChildERC20BridgeErrors.ZeroAmount.selector; + allowedErrors[1] = IChildERC20BridgeErrors.NoGas.selector; + allowedErrors[2] = IChildERC20BridgeErrors.InsufficientValue.selector; + fl.errAllow(bytes4(returnData), allowedErrors, CLDREV_08); + } + + /** + * @custom:invariant CLDREV-09: withdrawTo never reverts unexpectedly + */ + function invariant_CLDREV_09(bytes memory returnData) internal { + if (states[1].childBridgePaused) { + return; + } + bytes4[] memory allowedErrors = new bytes4[](2); + allowedErrors[0] = IChildERC20BridgeErrors.ZeroAmount.selector; + allowedErrors[1] = IChildERC20BridgeErrors.NoGas.selector; + fl.errAllow(bytes4(returnData), allowedErrors, CLDREV_09); + } + + /** + * @custom:invariant CLDREV-10: withdrawWIMX never reverts unexpectedly + */ + function invariant_CLDREV_10(bytes memory returnData) internal { + if (states[1].childBridgePaused) { + return; + } + bytes4[] memory allowedErrors = new bytes4[](2); + allowedErrors[0] = IChildERC20BridgeErrors.ZeroAmount.selector; + allowedErrors[1] = IChildERC20BridgeErrors.NoGas.selector; + fl.errAllow(bytes4(returnData), allowedErrors, CLDREV_10); + } + + /** + * @custom:invariant CLDREV-11: withdrawWIMXTo never reverts unexpectedly + */ + function invariant_CLDREV_11(bytes memory returnData) internal { + if (states[1].childBridgePaused) { + return; + } + bytes4[] memory allowedErrors = new bytes4[](2); + allowedErrors[0] = IChildERC20BridgeErrors.ZeroAmount.selector; + allowedErrors[1] = IChildERC20BridgeErrors.NoGas.selector; + fl.errAllow(bytes4(returnData), allowedErrors, CLDREV_11); + } +} diff --git a/test/fuzzing/properties/Properties_FLRT.sol b/test/fuzzing/properties/Properties_FLRT.sol new file mode 100644 index 00000000..7390c827 --- /dev/null +++ b/test/fuzzing/properties/Properties_FLRT.sol @@ -0,0 +1,177 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.0; + +import "./PropertiesBase.sol"; + +/** + * @title Properties_FLRT + * @author 0xScourgedev + * @notice Contains all FLRT invariants + */ +abstract contract Properties_FLRT is PropertiesBase { + /////////////////////////////////////////////////////////////////////////////////////////////// + // INVARIANTS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * @custom:invariant FLRT-01: If imxCumulativeDepositLimit is not 0, the rootIMX token balance + * of the root bridge should always be less than or equal to imxCumulativeDepositLimit + */ + function invariant_FLRT_01() internal { + if (states[1].imxCumulativeDepositLimit != 0) { + fl.lte(states[1].rootIMXTokenBalOfRootBridge, states[1].imxCumulativeDepositLimit, FLRT_01); + } + } + + /** + * @custom:invariant FLRT-02: If the withdrawal amount is greater than the largeTransferThreshold, + * the withdrawal must be added to the user's withdrawal queue + */ + function invariant_FLRT_02(address token, address recipient, uint256 amount) internal { + if (amount > states[1].tokenStates[token].largeTransferThresholds) { + fl.eq( + states[1].actorStates[recipient].queueLength, states[0].actorStates[recipient].queueLength + 1, FLRT_02 + ); + } + } + + /** + * @custom:invariant FLRT-03: If the bucket capacity for a token is 0, then any withdrawal of + * that token must be added to the user's withdrawal queue + */ + function invariant_FLRT_03(address token, address recipient) internal { + if (states[1].tokenStates[token].capacity == 0) { + fl.eq( + states[1].actorStates[recipient].queueLength, states[0].actorStates[recipient].queueLength + 1, FLRT_03 + ); + } + } + + /** + * @custom:invariant FLRT-04: If withdrawalQueueActivated is true, then any withdrawal of + * any token must be added to the user's withdrawal queue + */ + function invariant_FLRT_04(address recipient) internal { + if (states[0].withdrawalQueueActivated) { + fl.eq( + states[1].actorStates[recipient].queueLength, states[0].actorStates[recipient].queueLength + 1, FLRT_04 + ); + } + } + + /** + * @custom:invariant FLRT-05: If finaliseQueuedWithdrawal is successfully called, then the + * queued withdrawal initiated timestamp plus the withdrawal delay must be less than or equal + * to the current block timestamp + */ + function invariant_FLRT_05(FlowRateWithdrawalQueue.PendingWithdrawal memory withdrawal) internal { + fl.lte(withdrawal.timestamp + states[1].withdrawalDelay, block.timestamp, FLRT_05); + } + + /** + * @custom:invariant FLRT-06: After a successful withdrawal of a non-zero amount on the root + * bridge, if the bucket capacity is not zero, then the token bucket depth is never equal to + * the bucket capacity + */ + function invariant_FLRT_06(address token, uint256 amount) internal { + if (amount == 0) return; + + if (states[1].tokenStates[token].capacity == 0) return; + + fl.neq(states[1].tokenStates[token].depth, states[1].tokenStates[token].capacity, FLRT_06); + } + + /** + * @custom:invariant FLRT-07: After a successful withdraw on the root bridge, if the + * withdrawn amount is less than the minimum of the refill rate multiplied by the time + * elapsed since last withdrawal and the bucket capacity minus the prior real bucket depth, + * then the bucket depth strictly increases + */ + function invariant_FLRT_07(address token, uint256 amount) internal { + uint256 realDepth = fl.min(states[0].tokenStates[token].capacity, states[0].tokenStates[token].depth); + + if ( + amount + >= fl.min( + states[0].tokenStates[token].refillRate * (block.timestamp - states[0].tokenStates[token].refillTime), + states[0].tokenStates[token].capacity - realDepth + ) + ) return; + + fl.gt(states[1].tokenStates[token].depth, states[0].tokenStates[token].depth, FLRT_07); + } + + /** + * @custom:invariant FLRT-08: After a successful withdraw on the root bridge, the bucket depth + * is always less than or equal to the bucket capacity + */ + function invariant_FLRT_08(address token) internal { + fl.lte(states[1].tokenStates[token].depth, states[1].tokenStates[token].capacity, FLRT_08); + } + + /** + * @custom:invariant FLRT-09: When the withdrawalQueue is not activated, after a successful + * withdrawal of an amount less than the largeTransferThreshold on the root bridge and a withdrawal + * was added to the user's withdrawal queue, the bucket depth is always 0 + */ + function invariant_FLRT_09(address token, address recipient, uint256 amount) internal { + if (states[0].withdrawalQueueActivated) return; + + if (amount >= states[1].tokenStates[token].largeTransferThresholds) { + return; + } + + if (states[1].actorStates[recipient].queueLength == states[0].actorStates[recipient].queueLength) return; + + fl.eq(states[1].tokenStates[token].depth, 0, FLRT_09); + } + + /** + * @custom:invariant FLRT-10: When the withdrawalQueue is not activated, after a successful + * withdrawal of an amount less than the largeTransferThreshold on the root bridge, if the + * withdrawn amount is greater or equal to minimum of the refill rate multiplied by the time + * elapsed since last withdrawal plus the prior real bucket depth and the bucket capacity, + * then the withdrawal must be added to the user's withdrawal queue + */ + function invariant_FLRT_10(address token, address recipient, uint256 amount) internal { + if (states[0].withdrawalQueueActivated) return; + + if (amount >= states[1].tokenStates[token].largeTransferThresholds) { + return; + } + + uint256 realDepth = fl.min(states[0].tokenStates[token].capacity, states[0].tokenStates[token].depth); + + if ( + amount + < fl.min( + states[0].tokenStates[token].refillRate * (block.timestamp - states[0].tokenStates[token].refillTime) + + realDepth, + states[0].tokenStates[token].capacity + ) + ) return; + + fl.eq(states[1].actorStates[recipient].queueLength, states[0].actorStates[recipient].queueLength + 1, FLRT_10); + } + + /** + * @custom:invariant FLRT-11: If the depth of the token bucket is non-zero before a successful + * withdrawal on the root bridge and the depth of the token bucket is zero after the withdrawal, + * then the withdrawal queue must be activated + */ + function invariant_FLRT_11(address token) internal { + if (states[0].tokenStates[token].depth == 0 || states[1].tokenStates[token].depth != 0) return; + + fl.t(states[1].withdrawalQueueActivated, FLRT_11); + } + + /** + * @custom:invariant FLRT-12: After each successful withdrawal on the root bridge for a token with + * a non-zero capacity, the bucket refillTime is always updated to the current block timestamp + */ + function invariant_FLRT_12(address token) internal { + if (states[1].tokenStates[token].capacity == 0) return; + + fl.eq(states[1].tokenStates[token].refillTime, block.timestamp, FLRT_12); + } +} diff --git a/test/fuzzing/properties/Properties_PAUSE.sol b/test/fuzzing/properties/Properties_PAUSE.sol new file mode 100644 index 00000000..1c149cf6 --- /dev/null +++ b/test/fuzzing/properties/Properties_PAUSE.sol @@ -0,0 +1,159 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.0; + +import "./PropertiesBase.sol"; + +/** + * @title Properties_PAUSE + * @author 0xScourgedev + * @notice Contains all PAUSE invariants + */ +abstract contract Properties_PAUSE is PropertiesBase { + /////////////////////////////////////////////////////////////////////////////////////////////// + // INVARIANTS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * @custom:invariant PAUSE-01: deposit always reverts when the root bridge is paused + */ + function invariant_PAUSE_01() internal { + if (states[1].rootBridgePaused) { + fl.t(false, PAUSE_01); + } + } + + /** + * @custom:invariant PAUSE-02: depositETH always reverts when the root bridge is paused + */ + function invariant_PAUSE_02() internal { + if (states[1].rootBridgePaused) { + fl.t(false, PAUSE_02); + } + } + + /** + * @custom:invariant PAUSE-03: depositTo always reverts when the root bridge is paused + */ + function invariant_PAUSE_03() internal { + if (states[1].rootBridgePaused) { + fl.t(false, PAUSE_03); + } + } + + /** + * @custom:invariant PAUSE-04: depositToETH always reverts when the root bridge is paused + */ + function invariant_PAUSE_04() internal { + if (states[1].rootBridgePaused) { + fl.t(false, PAUSE_04); + } + } + + /** + * @custom:invariant PAUSE-05: finaliseQueuedWithdrawal always reverts when the root bridge is paused + */ + function invariant_PAUSE_05() internal { + if (states[1].rootBridgePaused) { + fl.t(false, PAUSE_05); + } + } + + /** + * @custom:invariant PAUSE-06: finaliseQueuedWithdrawalAggregated always reverts when the root bridge is paused + */ + function invariant_PAUSE_06() internal { + if (states[1].rootBridgePaused) { + fl.t(false, PAUSE_06); + } + } + + /** + * @custom:invariant PAUSE-07: onMessageReceive for the root bridge always reverts when the root bridge is paused + */ + function invariant_PAUSE_07() internal { + if (states[1].rootBridgePaused) { + fl.t(false, PAUSE_07); + } + } + + /** + * @custom:invariant PAUSE-08: withdraw always reverts when the child bridge is paused + */ + function invariant_PAUSE_08() internal { + if (states[1].childBridgePaused) { + fl.t(false, PAUSE_08); + } + } + + /** + * @custom:invariant PAUSE-09: withdrawETH always reverts when the child bridge is paused + */ + function invariant_PAUSE_09() internal { + if (states[1].childBridgePaused) { + fl.t(false, PAUSE_09); + } + } + + /** + * @custom:invariant PAUSE-10: withdrawETHTo always reverts when the child bridge is paused + */ + function invariant_PAUSE_10() internal { + if (states[1].childBridgePaused) { + fl.t(false, PAUSE_10); + } + } + + /** + * @custom:invariant PAUSE-11: withdrawIMX always reverts when the child bridge is paused + */ + function invariant_PAUSE_11() internal { + if (states[1].childBridgePaused) { + fl.t(false, PAUSE_11); + } + } + + /** + * @custom:invariant PAUSE-12: withdrawIMXTo always reverts when the child bridge is paused + */ + function invariant_PAUSE_12() internal { + if (states[1].childBridgePaused) { + fl.t(false, PAUSE_12); + } + } + + /** + * @custom:invariant PAUSE-13: withdrawTo always reverts when the child bridge is paused + */ + function invariant_PAUSE_13() internal { + if (states[1].childBridgePaused) { + fl.t(false, PAUSE_13); + } + } + + /** + * @custom:invariant PAUSE-14: withdrawWIMX always reverts when the child bridge is paused + */ + function invariant_PAUSE_14() internal { + if (states[1].childBridgePaused) { + fl.t(false, PAUSE_14); + } + } + + /** + * @custom:invariant PAUSE-15: withdrawWIMXTo always reverts when the child bridge is paused + */ + function invariant_PAUSE_15() internal { + if (states[1].childBridgePaused) { + fl.t(false, PAUSE_15); + } + } + + /** + * @custom:invariant PAUSE-16: onMessageReceive for the child bridge always reverts when the child bridge is paused + */ + function invariant_PAUSE_16() internal { + if (states[1].childBridgePaused) { + fl.t(false, PAUSE_16); + } + } +} diff --git a/test/fuzzing/properties/Properties_RTREV.sol b/test/fuzzing/properties/Properties_RTREV.sol new file mode 100644 index 00000000..bd91f986 --- /dev/null +++ b/test/fuzzing/properties/Properties_RTREV.sol @@ -0,0 +1,172 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.0; + +import "./PropertiesBase.sol"; + +/** + * @title Properties_RTREV + * @author 0xScourgedev + * @notice Contains all RTREV invariants + */ +abstract contract Properties_RTREV is PropertiesBase { + /////////////////////////////////////////////////////////////////////////////////////////////// + // INVARIANTS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * @custom:invariant RTREV-01: activateWithdrawalQueue never reverts + */ + function invariant_RTREV_01(bytes memory returnData) internal { + bytes4[] memory allowedErrors = new bytes4[](0); + fl.errAllow(bytes4(returnData), allowedErrors, RTREV_01); + } + + /** + * @custom:invariant RTREV-02: deactivateWithdrawalQueue never reverts + */ + function invariant_RTREV_02(bytes memory returnData) internal { + bytes4[] memory allowedErrors = new bytes4[](0); + fl.errAllow(bytes4(returnData), allowedErrors, RTREV_02); + } + + /** + * @custom:invariant RTREV-03: deposit never reverts unexpectedly + */ + function invariant_RTREV_03(bytes memory returnData) internal { + if (states[1].rootBridgePaused) { + return; + } + bytes4[] memory allowedErrors = new bytes4[](3); + allowedErrors[0] = IRootERC20BridgeErrors.NoGas.selector; + allowedErrors[1] = IRootERC20BridgeErrors.ZeroAmount.selector; + allowedErrors[2] = IRootERC20BridgeErrors.ImxDepositLimitExceeded.selector; + fl.errAllow(bytes4(returnData), allowedErrors, RTREV_03); + } + + /** + * @custom:invariant RTREV-04: depositETH never reverts unexpectedly + */ + function invariant_RTREV_04(bytes memory returnData) internal { + if (states[1].rootBridgePaused) { + return; + } + bytes4[] memory allowedErrors = new bytes4[](3); + allowedErrors[0] = IRootERC20BridgeErrors.NoGas.selector; + allowedErrors[1] = IRootERC20BridgeErrors.ZeroAmount.selector; + allowedErrors[2] = IRootERC20BridgeErrors.InsufficientValue.selector; + fl.errAllow(bytes4(returnData), allowedErrors, RTREV_04); + } + + /** + * @custom:invariant RTREV-05: depositTo never reverts unexpectedly + */ + function invariant_RTREV_05(bytes memory returnData) internal { + if (states[1].rootBridgePaused) { + return; + } + bytes4[] memory allowedErrors = new bytes4[](3); + allowedErrors[0] = IRootERC20BridgeErrors.NoGas.selector; + allowedErrors[1] = IRootERC20BridgeErrors.ZeroAmount.selector; + allowedErrors[2] = IRootERC20BridgeErrors.ImxDepositLimitExceeded.selector; + fl.errAllow(bytes4(returnData), allowedErrors, RTREV_05); + } + + /** + * @custom:invariant RTREV-06: depositToETH never reverts unexpectedly + */ + function invariant_RTREV_06(bytes memory returnData) internal { + if (states[1].rootBridgePaused) { + return; + } + bytes4[] memory allowedErrors = new bytes4[](3); + allowedErrors[0] = IRootERC20BridgeErrors.NoGas.selector; + allowedErrors[1] = IRootERC20BridgeErrors.ZeroAmount.selector; + allowedErrors[2] = IRootERC20BridgeErrors.InsufficientValue.selector; + fl.errAllow(bytes4(returnData), allowedErrors, RTREV_06); + } + + /** + * @custom:invariant RTREV-07: finaliseQueuedWithdrawal never reverts unexpectedly + */ + function invariant_RTREV_07(bytes memory returnData) internal { + if (states[1].rootBridgePaused) { + return; + } + bytes4[] memory allowedErrors = new bytes4[](3); + allowedErrors[0] = IFlowRateWithdrawalQueueErrors.IndexOutsideWithdrawalQueue.selector; + allowedErrors[1] = IFlowRateWithdrawalQueueErrors.WithdrawalAlreadyProcessed.selector; + allowedErrors[2] = IFlowRateWithdrawalQueueErrors.WithdrawalRequestTooEarly.selector; + fl.errAllow(bytes4(returnData), allowedErrors, RTREV_07); + } + + /** + * @custom:invariant RTREV-08: finaliseQueuedWithdrawalAggregated never reverts unexpectedly + */ + function invariant_RTREV_08(bytes memory returnData) internal { + if (states[1].rootBridgePaused) { + return; + } + bytes4[] memory allowedErrors = new bytes4[](5); + allowedErrors[0] = IFlowRateWithdrawalQueueErrors.IndexOutsideWithdrawalQueue.selector; + allowedErrors[1] = IFlowRateWithdrawalQueueErrors.WithdrawalAlreadyProcessed.selector; + allowedErrors[2] = IFlowRateWithdrawalQueueErrors.WithdrawalRequestTooEarly.selector; + allowedErrors[3] = IRootERC20BridgeFlowRateErrors.ProvideAtLeastOneIndex.selector; + allowedErrors[4] = IRootERC20BridgeFlowRateErrors.MixedTokens.selector; + fl.errAllow(bytes4(returnData), allowedErrors, RTREV_08); + } + + /** + * @custom:invariant RTREV-09: onMessageReceive for the root bridge never reverts unexpectedly + */ + function invariant_RTREV_09(bytes memory returnData) internal { + if (states[1].rootBridgePaused) { + return; + } + bytes4[] memory allowedErrors = new bytes4[](1); + allowedErrors[0] = IRootERC20BridgeErrors.InvalidData.selector; + fl.errAllow(bytes4(returnData), allowedErrors, RTREV_09); + } + + /** + * @custom:invariant RTREV-10: pause for the root bridge never reverts + */ + function invariant_RTREV_10(bytes memory returnData) internal { + bytes4[] memory allowedErrors = new bytes4[](0); + fl.errAllow(bytes4(returnData), allowedErrors, RTREV_10); + } + + /** + * @custom:invariant RTREV-11: setRateControlThreshold never reverts + */ + function invariant_RTREV_11(bytes memory returnData) internal { + bytes4[] memory allowedErrors = new bytes4[](2); + allowedErrors[0] = FlowRateDetection.InvalidCapacity.selector; + allowedErrors[1] = FlowRateDetection.InvalidRefillRate.selector; + fl.errAllow(bytes4(returnData), allowedErrors, RTREV_11); + } + + /** + * @custom:invariant RTREV-12: setWithdrawalDelay never reverts + */ + function invariant_RTREV_12(bytes memory returnData) internal { + bytes4[] memory allowedErrors = new bytes4[](0); + fl.errAllow(bytes4(returnData), allowedErrors, RTREV_12); + } + + /** + * @custom:invariant RTREV-13: unpause for the root bridge never reverts + */ + function invariant_RTREV_13(bytes memory returnData) internal { + bytes4[] memory allowedErrors = new bytes4[](0); + fl.errAllow(bytes4(returnData), allowedErrors, RTREV_13); + } + + /** + * @custom:invariant RTREV-14: updateImxCumulativeDepositLimit never reverts unexpectedly + */ + function invariant_RTREV_14(bytes memory returnData) internal { + bytes4[] memory allowedErrors = new bytes4[](1); + allowedErrors[0] = IRootERC20BridgeErrors.ImxDepositLimitTooLow.selector; + fl.errAllow(bytes4(returnData), allowedErrors, RTREV_14); + } +} diff --git a/test/fuzzing/properties/Properties_SPLY.sol b/test/fuzzing/properties/Properties_SPLY.sol new file mode 100644 index 00000000..06d9b067 --- /dev/null +++ b/test/fuzzing/properties/Properties_SPLY.sol @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.0; + +import "./PropertiesBase.sol"; + +/** + * @title Properties_SPLY + * @author 0xScourgedev + * @notice Contains all SPLY invariants + */ +abstract contract Properties_SPLY is PropertiesBase { + /////////////////////////////////////////////////////////////////////////////////////////////// + // INVARIANTS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * @custom:invariant SPLY-01: The sum of the token balances of each user and the bridges should + * be exactly equal to the token total supply + */ + function invariant_SPLY_01(address token) internal { + if (token == NATIVE_ETH || token == address(0)) return; + uint256 totalBal = 0; + + for (uint256 i = 0; i < USERS.length; i++) { + totalBal += states[1].tokenStates[token].balances[USERS[i]]; + } + + totalBal += states[1].tokenStates[token].balances[rootERC20BridgeFlowRate]; + totalBal += states[1].tokenStates[token].balances[childERC20Bridge]; + + fl.eq(totalBal, states[1].tokenStates[token].totalSupply, SPLY_01); + } + + /** + * @custom:invariant SPLY-02: The total supply of root WETH must be decreased by the amount + * of WETH deposited by the user + */ + function invariant_SPLY_02(address token, uint256 amount) internal { + if (token != wETH) return; + + fl.eq(states[1].tokenStates[token].totalSupply, states[0].tokenStates[token].totalSupply - amount, SPLY_02); + } + + /** + * @custom:invariant The total supply of child ERC20s must be increased by exactly the amount + * of tokens deposited by the user + */ + function invariant_SPLY_03(address token, uint256 amount) internal { + if (token == NATIVE_IMX || token == address(0)) return; + + fl.eq(states[1].tokenStates[token].totalSupply, states[0].tokenStates[token].totalSupply + amount, SPLY_03); + } + + /** + * @custom:invariant The total supply of child ERC20s must be decreased by exactly the amount + * of tokens withdrawn by the user + */ + function invariant_SPLY_04(address token, uint256 amount) internal { + fl.eq(states[1].tokenStates[token].totalSupply, states[0].tokenStates[token].totalSupply - amount, SPLY_04); + } +} diff --git a/test/fuzzing/util/FunctionCalls.sol b/test/fuzzing/util/FunctionCalls.sol new file mode 100644 index 00000000..d445ed8a --- /dev/null +++ b/test/fuzzing/util/FunctionCalls.sol @@ -0,0 +1,335 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.0; + +import "@perimetersec/fuzzlib/src/FuzzBase.sol"; + +import "../helper/FuzzStorageVariables.sol"; + +/** + * @title FunctionCalls + * @author 0xScourgedev + * @notice Contains the function calls for all of the handlers + */ +abstract contract FunctionCalls is FuzzBase, FuzzStorageVariables { + /////////////////////////////////////////////////////////////////////////////////////////////// + // EVENTS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + // Functions from ChildERC20Bridge + + event OnMessageReceiveChildCall(bytes data); + event PauseChildCall(); + event UnpauseChildCall(); + event WithdrawCall(address childToken, uint256 amount); + event WithdrawETHCall(uint256 amount); + event WithdrawETHToCall(address receiver, uint256 amount); + event WithdrawIMXCall(uint256 amount); + event WithdrawIMXToCall(address receiver, uint256 amount); + event WithdrawToCall(address childToken, address receiver, uint256 amount); + event WithdrawWIMXCall(uint256 amount); + event WithdrawWIMXToCall(address receiver, uint256 amount); + + // Functions from RootERC20BridgeFlowRate + + event ActivateWithdrawalQueueCall(); + event DeactivateWithdrawalQueueCall(); + event DepositCall(address rootToken, uint256 amount); + event DepositETHCall(uint256 amount); + event DepositToCall(address rootToken, address receiver, uint256 amount); + event DepositToETHCall(address receiver, uint256 amount); + event FinaliseQueuedWithdrawalCall(address receiver, uint256 index); + event FinaliseQueuedWithdrawalsAggregatedCall(address receiver, address token, uint256[] indices); + event OnMessageReceiveRootCall(bytes data); + event PauseRootCall(); + event SetRateControlThresholdCall( + address token, uint256 capacity, uint256 refillRate, uint256 largeTransferThreshold + ); + event SetWithdrawalDelayCall(uint256 delay); + event UnpauseRootCall(); + event UpdateImxCumulativeDepositLimitCall(uint256 newImxCumulativeDepositLimit); + event UpdateRootBridgeAdaptorCall(address newRootBridgeAdaptor); + + /////////////////////////////////////////////////////////////////////////////////////////////// + // FUNCTIONS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + // Functions from ChildERC20Bridge + + function _onMessageReceiveChildCall(bytes memory data) internal returns (bool success, bytes memory returnData) { + emit OnMessageReceiveChildCall(data); + + vm.prank(currentActor); + + (success, returnData) = + mockAdaptorChild.call{gas: 1000000}(abi.encodeWithSelector(MockAdaptor.onMessageReceive.selector, data)); + } + + function _pauseChildCall() internal returns (bool success, bytes memory returnData) { + emit PauseChildCall(); + + (success, returnData) = childERC20Bridge.call{gas: 1000000}(abi.encodeWithSelector(BridgeRoles.pause.selector)); + } + + function _unpauseChildCall() internal returns (bool success, bytes memory returnData) { + emit UnpauseChildCall(); + + (success, returnData) = + childERC20Bridge.call{gas: 1000000}(abi.encodeWithSelector(BridgeRoles.unpause.selector)); + } + + function _withdrawCall(address childToken, uint256 amount, uint256 value) + internal + returns (bool success, bytes memory returnData) + { + emit WithdrawCall(childToken, amount); + + vm.prank(currentActor); + + (success, returnData) = childERC20Bridge.call{gas: 1000000, value: value}( + abi.encodeWithSelector(ChildERC20Bridge.withdraw.selector, childToken, amount) + ); + } + + function _withdrawETHCall(uint256 amount, uint256 value) internal returns (bool success, bytes memory returnData) { + emit WithdrawETHCall(amount); + + vm.prank(currentActor); + + (success, returnData) = childERC20Bridge.call{gas: 1000000, value: value}( + abi.encodeWithSelector(ChildERC20Bridge.withdrawETH.selector, amount) + ); + } + + function _withdrawETHToCall(address receiver, uint256 amount, uint256 value) + internal + returns (bool success, bytes memory returnData) + { + emit WithdrawETHToCall(receiver, amount); + + vm.prank(currentActor); + + (success, returnData) = childERC20Bridge.call{gas: 1000000, value: value}( + abi.encodeWithSelector(ChildERC20Bridge.withdrawETHTo.selector, receiver, amount) + ); + } + + function _withdrawIMXCall(uint256 amount, uint256 value) internal returns (bool success, bytes memory returnData) { + emit WithdrawIMXCall(amount); + + vm.prank(currentActor); + + (success, returnData) = childERC20Bridge.call{gas: 1000000, value: value}( + abi.encodeWithSelector(ChildERC20Bridge.withdrawIMX.selector, amount) + ); + } + + function _withdrawIMXToCall(address receiver, uint256 amount, uint256 value) + internal + returns (bool success, bytes memory returnData) + { + emit WithdrawIMXToCall(receiver, amount); + + vm.prank(currentActor); + + (success, returnData) = childERC20Bridge.call{gas: 1000000, value: value}( + abi.encodeWithSelector(ChildERC20Bridge.withdrawIMXTo.selector, receiver, amount) + ); + } + + function _withdrawToCall(address childToken, address receiver, uint256 amount, uint256 value) + internal + returns (bool success, bytes memory returnData) + { + emit WithdrawToCall(childToken, receiver, amount); + + vm.prank(currentActor); + + (success, returnData) = childERC20Bridge.call{gas: 1000000, value: value}( + abi.encodeWithSelector(ChildERC20Bridge.withdrawTo.selector, childToken, receiver, amount) + ); + } + + function _withdrawWIMXCall(uint256 amount, uint256 value) + internal + returns (bool success, bytes memory returnData) + { + emit WithdrawWIMXCall(amount); + + vm.prank(currentActor); + + (success, returnData) = childERC20Bridge.call{gas: 1000000, value: value}( + abi.encodeWithSelector(ChildERC20Bridge.withdrawWIMX.selector, amount) + ); + } + + function _withdrawWIMXToCall(address receiver, uint256 amount, uint256 value) + internal + returns (bool success, bytes memory returnData) + { + emit WithdrawWIMXToCall(receiver, amount); + + vm.prank(currentActor); + + (success, returnData) = childERC20Bridge.call{gas: 1000000, value: value}( + abi.encodeWithSelector(ChildERC20Bridge.withdrawWIMXTo.selector, receiver, amount) + ); + } + + // Functions from RootERC20BridgeFlowRate + + function _activateWithdrawalQueueCall() internal returns (bool success, bytes memory returnData) { + emit ActivateWithdrawalQueueCall(); + + (success, returnData) = rootERC20BridgeFlowRate.call{gas: 1000000}( + abi.encodeWithSelector(RootERC20BridgeFlowRate.activateWithdrawalQueue.selector) + ); + } + + function _deactivateWithdrawalQueueCall() internal returns (bool success, bytes memory returnData) { + emit DeactivateWithdrawalQueueCall(); + + (success, returnData) = rootERC20BridgeFlowRate.call{gas: 1000000}( + abi.encodeWithSelector(RootERC20BridgeFlowRate.deactivateWithdrawalQueue.selector) + ); + } + + function _depositCall(address rootToken, uint256 amount, uint256 value) + internal + returns (bool success, bytes memory returnData) + { + emit DepositCall(rootToken, amount); + + vm.prank(currentActor); + + (success, returnData) = rootERC20BridgeFlowRate.call{gas: 1000000, value: value}( + abi.encodeWithSelector(RootERC20Bridge.deposit.selector, rootToken, amount) + ); + } + + function _depositETHCall(uint256 amount, uint256 value) internal returns (bool success, bytes memory returnData) { + emit DepositETHCall(amount); + + vm.prank(currentActor); + + (success, returnData) = rootERC20BridgeFlowRate.call{gas: 1000000, value: value}( + abi.encodeWithSelector(RootERC20Bridge.depositETH.selector, amount) + ); + } + + function _depositToCall(address rootToken, address receiver, uint256 amount, uint256 value) + internal + returns (bool success, bytes memory returnData) + { + emit DepositToCall(rootToken, receiver, amount); + + vm.prank(currentActor); + + (success, returnData) = rootERC20BridgeFlowRate.call{gas: 1000000, value: value}( + abi.encodeWithSelector(RootERC20Bridge.depositTo.selector, rootToken, receiver, amount) + ); + } + + function _depositToETHCall(address receiver, uint256 amount, uint256 value) + internal + returns (bool success, bytes memory returnData) + { + emit DepositToETHCall(receiver, amount); + + vm.prank(currentActor); + + (success, returnData) = rootERC20BridgeFlowRate.call{gas: 1000000, value: value}( + abi.encodeWithSelector(RootERC20Bridge.depositToETH.selector, receiver, amount) + ); + } + + function _finaliseQueuedWithdrawalCall(address receiver, uint256 index) + internal + returns (bool success, bytes memory returnData) + { + emit FinaliseQueuedWithdrawalCall(receiver, index); + + vm.prank(currentActor); + + (success, returnData) = rootERC20BridgeFlowRate.call{gas: 1000000}( + abi.encodeWithSelector(RootERC20BridgeFlowRate.finaliseQueuedWithdrawal.selector, receiver, index) + ); + } + + function _finaliseQueuedWithdrawalsAggregatedCall(address receiver, address token, uint256[] memory indices) + internal + returns (bool success, bytes memory returnData) + { + emit FinaliseQueuedWithdrawalsAggregatedCall(receiver, token, indices); + + vm.prank(currentActor); + + (success, returnData) = rootERC20BridgeFlowRate.call{gas: 1000000}( + abi.encodeWithSelector( + RootERC20BridgeFlowRate.finaliseQueuedWithdrawalsAggregated.selector, receiver, token, indices + ) + ); + } + + function _onMessageReceiveRootCall(bytes memory data) internal returns (bool success, bytes memory returnData) { + emit OnMessageReceiveRootCall(data); + + vm.prank(currentActor); + + (success, returnData) = + mockAdaptorRoot.call{gas: 1000000}(abi.encodeWithSelector(MockAdaptor.onMessageReceive.selector, data)); + } + + function _pauseRootCall() internal returns (bool success, bytes memory returnData) { + emit PauseRootCall(); + + (success, returnData) = + rootERC20BridgeFlowRate.call{gas: 1000000}(abi.encodeWithSelector(BridgeRoles.pause.selector)); + } + + function _setRateControlThresholdCall( + address token, + uint256 capacity, + uint256 refillRate, + uint256 largeTransferThreshold + ) internal returns (bool success, bytes memory returnData) { + emit SetRateControlThresholdCall(token, capacity, refillRate, largeTransferThreshold); + + (success, returnData) = rootERC20BridgeFlowRate.call{gas: 1000000}( + abi.encodeWithSelector( + RootERC20BridgeFlowRate.setRateControlThreshold.selector, + token, + capacity, + refillRate, + largeTransferThreshold + ) + ); + } + + function _setWithdrawalDelayCall(uint256 delay) internal returns (bool success, bytes memory returnData) { + emit SetWithdrawalDelayCall(delay); + + (success, returnData) = rootERC20BridgeFlowRate.call{gas: 1000000}( + abi.encodeWithSelector(RootERC20BridgeFlowRate.setWithdrawalDelay.selector, delay) + ); + } + + function _unpauseRootCall() internal returns (bool success, bytes memory returnData) { + emit UnpauseRootCall(); + + (success, returnData) = + rootERC20BridgeFlowRate.call{gas: 1000000}(abi.encodeWithSelector(BridgeRoles.unpause.selector)); + } + + function _updateImxCumulativeDepositLimitCall(uint256 newImxCumulativeDepositLimit) + internal + returns (bool success, bytes memory returnData) + { + emit UpdateImxCumulativeDepositLimitCall(newImxCumulativeDepositLimit); + + (success, returnData) = rootERC20BridgeFlowRate.call{gas: 1000000}( + abi.encodeWithSelector( + RootERC20Bridge.updateImxCumulativeDepositLimit.selector, newImxCumulativeDepositLimit + ) + ); + } +} diff --git a/test/fuzzing/util/FuzzConstants.sol b/test/fuzzing/util/FuzzConstants.sol new file mode 100644 index 00000000..0673322e --- /dev/null +++ b/test/fuzzing/util/FuzzConstants.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.0; + +import "@perimetersec/fuzzlib/src/IHevm.sol"; + +/** + * @title FuzzConstants + * @author 0xScourgedev + * @notice Constants and assumptions for the fuzzing suite + */ +abstract contract FuzzConstants { + /////////////////////////////////////////////////////////////////////////////////////////////// + // FUZZ CONFIGS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + address internal constant USER1 = address(0x10001); + address internal constant USER2 = address(0x20001); + address internal constant USER3 = address(0x30001); + address[] internal USERS = [USER1, USER2, USER3]; + + /////////////////////////////////////////////////////////////////////////////////////////////// + // CONSTANTS // + /////////////////////////////////////////////////////////////////////////////////////////////// + + uint256 internal constant COVERAGE_GAP = 1000; + uint256 internal constant MAX_REFILL_RATE_GAP = 3600; + uint256 internal constant MAX_WITHDRAWAL_DELAY = 1209600; // 2 weeks + bytes32 internal constant WITHDRAW_SIG = keccak256("WITHDRAW"); + bytes32 internal constant DEPOSIT_SIG = keccak256("DEPOSIT"); + bytes32 internal constant NOTHING_SIG = keccak256("NOTHING"); + address internal constant NATIVE_ETH = address(0xeee); + address internal constant NATIVE_IMX = address(0xfff); + uint256 internal constant MAX_IN_QUEUE = 20; + uint256 internal constant EXECUTE_WHEN_PAUSED_ODDS = 100; // 1% chance of executing when paused + + uint256 internal constant IMX_DEPOSIT_LIMIT = 500_000 ether; + uint256 internal constant MAX_ERC20_BALANCE = 1_000_000_000; // 1 billion tokens as the max ERC20 balance + uint256 internal constant INITIAL_BALANCE = 500_000 ether; // 1 Billion USD worth of ETH at $2000/ETH + uint256 internal constant INITIAL_WETH_BALANCE = 500_000 ether; // 1 Billion USD worth of ETH at $2000/ETH +} diff --git a/yarn.lock b/yarn.lock index b92f83fd..f9ca9493 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1365,6 +1365,11 @@ resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-4.9.3.tgz#00d7a8cf35a475b160b3f0293a6403c511099364" integrity sha512-He3LieZ1pP2TNt5JbkPA4PNT9WC3gOTOlDcFGJW4Le4QKqwmiNJCRt44APfxMxvq7OugU/cqYuPcSBzOw38DAg== +"@perimetersec/fuzzlib@0.3.0": + version "0.3.0" + resolved "https://registry.yarnpkg.com/@perimetersec/fuzzlib/-/fuzzlib-0.3.0.tgz#4475b3fd8d0ece81c8617e3e82a58686c7c8677c" + integrity sha512-lrjaC5M+ImezTXZhdLOYhc6AFIyCP6neA9OOCGpIunOUmFNOaBbT80R9sfPSElLwDkMzBMbpUvdPt2uFvrPEAA== + "@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf"