Copy commit 625637a9e64fa298b3a80b3216382c3382656f97
Author: Kirill Varlamov <[email protected] >
Date: Tue Nov 28 03:12:40 2023 +0400
test: malicious DeGate operator stops Exchange (centralized control by EOA)
diff --git a/foundry.toml b/foundry.toml
index 25b918f9..aa662d8a 100644
--- a/foundry.toml
+++ b/foundry.toml
@@ -3,4 +3,5 @@ src = "src"
out = "out"
libs = ["lib"]
+solc = '0.8.23'
# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
diff --git a/src/Counter.sol b/src/Counter.sol
deleted file mode 100644
index aded7997..00000000
--- a/src/Counter.sol
+++ /dev/null
@@ -1,14 +0,0 @@
-// SPDX-License-Identifier: UNLICENSED
-pragma solidity ^0.8.13;
-
-contract Counter {
- uint256 public number;
-
- function setNumber(uint256 newNumber) public {
- number = newNumber;
- }
-
- function increment() public {
- number++;
- }
-}
diff --git a/test/Counter.t.sol b/test/Counter.t.sol
deleted file mode 100644
index e9b9e6ac..00000000
--- a/test/Counter.t.sol
+++ /dev/null
@@ -1,24 +0,0 @@
-// SPDX-License-Identifier: UNLICENSED
-pragma solidity ^0.8.13;
-
-import {Test, console2} from "forge-std/Test.sol";
-import {Counter} from "../src/Counter.sol";
-
-contract CounterTest is Test {
- Counter public counter;
-
- function setUp() public {
- counter = new Counter();
- counter.setNumber(0);
- }
-
- function test_Increment() public {
- counter.increment();
- assertEq(counter.number(), 1);
- }
-
- function testFuzz_SetNumber(uint256 x) public {
- counter.setNumber(x);
- assertEq(counter.number(), x);
- }
-}
diff --git a/test/MaliciousOperatorStopsExchangeContract.t.sol b/test/MaliciousOperatorStopsExchangeContract.t.sol
new file mode 100644
index 00000000..52685b34
--- /dev/null
+++ b/test/MaliciousOperatorStopsExchangeContract.t.sol
@@ -0,0 +1,65 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.10;
+
+import "forge-std/Test.sol";
+import "test/mock/IExchangeV3.sol";
+import "test/mock/ILoopringIOExchangeOwner.sol";
+
+contract ShutdownExchangeTest is Test {
+ event DepositParamsUpdate(
+ uint256 freeDepositMax, uint256 freeDepositRemained, uint256 freeSlotPerBlock, uint256 depositFee
+ );
+
+ IExchangeV3 exchange;
+ address exchangeOwner;
+ ILoopringIOExchangeOwner ioex;
+ address ioexOwner;
+
+ function setUp() public {
+ uint256 fork;
+ fork = vm.createFork("https://mainnet.infura.io/v3/bcb7b03333384d49957cc9b4b53daa1d");
+ vm.selectFork(fork);
+ exchange = IExchangeV3(0x9C07A72177c5A05410cA338823e790876E79D73B);
+ ioex = ILoopringIOExchangeOwner(0x9b93e47b7F61ad1358Bd47Cd01206708E85AE5eD);
+ ioexOwner = ioex.owner();
+ }
+
+ function testLoopringIOExchangeOwnerIsEOA() public {
+ // ioexOwner is EOA (i.e. centralized control)
+ assertFalse(isContract(ioexOwner));
+ }
+
+ function testExchangeShutdown() public {
+ // Exchange is operational before malicious admin shuts it down
+ assertFalse(exchange.isShutdown());
+ bytes memory data = abi.encodeWithSignature("shutdown()");
+ vm.prank(ioexOwner);
+ ioex.transact(data);
+ // Exchange stopped its operation as result
+ assertTrue(exchange.isShutdown());
+ }
+
+ function testSetMaxAgeDepositUntilWithdrawable() public {
+ assertEq(exchange.getMaxAgeDepositUntilWithdrawable(), 1296000);
+ bytes memory data = abi.encodeWithSignature("setMaxAgeDepositUntilWithdrawable(uint32)", 1);
+ vm.prank(ioexOwner);
+ ioex.transact(data);
+ assertEq(exchange.getMaxAgeDepositUntilWithdrawable(), 1);
+ }
+
+ function testExchangeSetDepositParams() public {
+ bytes memory data = abi.encodeWithSignature("setDepositParams(uint256,uint256,uint256,uint256)", 0, 0, 0, 0);
+ vm.expectEmit(true, true, true, true);
+ emit DepositParamsUpdate(0, 0, 0, 0);
+ vm.prank(ioexOwner);
+ ioex.transact(data);
+ }
+
+ function isContract(address _addr) public view returns (bool) {
+ uint32 size;
+ assembly {
+ size := extcodesize(_addr)
+ }
+ return (size > 0);
+ }
+}
diff --git a/test/mock/IExchangeV3.sol b/test/mock/IExchangeV3.sol
new file mode 100644
index 00000000..bdb9e1f8
--- /dev/null
+++ b/test/mock/IExchangeV3.sol
@@ -0,0 +1,121 @@
+// SPDX-License-Identifier: Apache-2.0
+pragma solidity ^0.8.0;
+
+import "./IOwnable.sol";
+
+interface IExchangeV3 is IOwnable {
+ // Event declarations
+ event DepositContractUpdate(address indexed depositContract);
+ event WithdrawExchangeFees(address indexed token, address indexed recipient);
+ event DepositParamsUpdate(
+ uint256 freeDepositMax, uint256 freeDepositRemained, uint256 freeSlotPerBlock, uint256 depositFee
+ );
+ event WithdrawalModeActivated(uint256 timestamp);
+ event WithdrawalRecipientUpdate(
+ address from, address to, address token, uint248 amount, uint32 storageID, address newRecipient
+ );
+ event TransactionApproved(address from, bytes32 transactionHash);
+ event TransactionsApproved(address[] owners, bytes32[] transactionHashes);
+ event Shutdown(uint256 timestamp);
+ event AllowOnchainTransferFrom(bool value);
+
+ struct Constants {
+ uint256 SNARK_SCALAR_FIELD;
+ uint256 MAX_OPEN_FORCED_REQUESTS;
+ uint256 MAX_AGE_FORCED_REQUEST_UNTIL_WITHDRAW_MODE;
+ uint256 TIMESTAMP_HALF_WINDOW_SIZE_IN_SECONDS;
+ uint256 MAX_NUM_ACCOUNTS;
+ uint256 MAX_NUM_TOKENS;
+ uint256 MIN_AGE_PROTOCOL_FEES_UNTIL_UPDATED;
+ uint256 MIN_TIME_IN_SHUTDOWN;
+ uint256 TX_DATA_AVAILABILITY_SIZE;
+ uint256 MAX_AGE_DEPOSIT_UNTIL_WITHDRAWABLE_UPPERBOUND;
+ uint256 MAX_FORCED_WITHDRAWAL_FEE;
+ uint256 DEFAULT_PROTOCOL_FEE_BIPS;
+ }
+
+ struct BlockInfo {
+ // The time the block was submitted on-chain.
+ uint32 timestamp;
+ // The public data hash of the block (the 28 most significant bytes).
+ bytes28 blockDataHash;
+ }
+
+ // Function declarations
+ function version() external pure returns (string memory);
+ function domainSeparator() external view returns (bytes32);
+ function initialize(address _loopring, address _owner, bytes32 _genesisMerkleRoot, bytes32 _genesisMerkleAssetRoot)
+ external;
+ function setDepositContract(address _depositContract) external;
+ function getDepositContract() external view returns (address);
+ function withdrawExchangeFees(address token, address recipient) external;
+ function setDepositParams(
+ uint256 freeDepositMax,
+ uint256 freeDepositRemained,
+ uint256 freeSlotPerBlock,
+ uint256 depositFee
+ ) external;
+ function isUserOrAgent(address from) external view returns (bool);
+ function getConstants() external pure returns (Constants memory);
+ function isInWithdrawalMode() external view returns (bool);
+ function isShutdown() external view returns (bool);
+ function registerToken(address tokenAddress) external returns (uint32);
+ function getTokenID(address tokenAddress) external view returns (uint32);
+ function getTokenAddress(uint32 tokenID) external view returns (address);
+ function getExchangeStake() external view returns (uint256);
+ function withdrawExchangeStake(address recipient) external returns (uint256);
+ function getProtocolFeeLastWithdrawnTime(address tokenAddress) external view returns (uint256);
+ function burnExchangeStake() external;
+ function getMerkleRoot() external view returns (bytes32);
+ function getMerkleAssetRoot() external view returns (bytes32);
+ function getBlockHeight() external view returns (uint256);
+ function getBlockInfo(uint256 blockIdx) external view returns (BlockInfo memory);
+ // function submitBlocks(ExchangeData.Block[] calldata blocks) external;
+ function getNumAvailableForcedSlots() external view returns (uint256);
+ function deposit(address from, address to, address tokenAddress, uint248 amount, bytes calldata extraData)
+ external
+ payable;
+ function getPendingDepositAmount(address from, address tokenAddress) external view returns (uint248);
+ function forceWithdraw(address from, address token, uint32 accountID) external payable;
+ function isForcedWithdrawalPending(uint32 accountID, address token) external view returns (bool);
+ // function withdrawFromMerkleTree(ExchangeData.MerkleProof calldata merkleProof) external;
+ function isWithdrawnInWithdrawalMode(uint32 accountID, address token) external view returns (bool);
+ function withdrawFromDepositRequest(address from, address token) external;
+ function withdrawFromApprovedWithdrawals(address[] calldata owners, address[] calldata tokens) external;
+ function getAmountWithdrawable(address from, address token) external view returns (uint256);
+ function notifyForcedRequestTooOld(uint32 accountID, address token) external;
+ function setWithdrawalRecipient(
+ address from,
+ address to,
+ address token,
+ uint248 amount,
+ uint32 storageID,
+ address newRecipient
+ ) external;
+ function getWithdrawalRecipient(address from, address to, address token, uint248 amount, uint32 storageID)
+ external
+ view
+ returns (address);
+ function onchainTransferFrom(address from, address to, address token, uint256 amount) external;
+ function approveTransaction(address from, bytes32 transactionHash) external;
+ function approveTransactions(address[] calldata owners, bytes32[] calldata transactionHashes) external;
+ function isTransactionApproved(address from, bytes32 transactionHash) external view returns (bool);
+ function getDomainSeparator() external view returns (bytes32);
+ function setMaxAgeDepositUntilWithdrawable(uint32 newValue) external returns (uint32);
+ function getMaxAgeDepositUntilWithdrawable() external view returns (uint32);
+ function shutdown() external returns (bool);
+ function getProtocolFeeValues()
+ external
+ view
+ returns (
+ uint32 syncedAt,
+ uint16 protocolFeeBips,
+ uint16 previousProtocolFeeBips,
+ uint32 executeTimeOfNextProtocolFeeBips,
+ uint16 nextProtocolFeeBips
+ );
+ function setAllowOnchainTransferFrom(bool value) external;
+ function getUnconfirmedBalance(address token) external view returns (uint256);
+ function getFreeDepositRemained() external view returns (uint256);
+ function getDepositBalance(address token) external view returns (uint248);
+}
diff --git a/test/mock/ILoopringIOExchangeOwner.sol b/test/mock/ILoopringIOExchangeOwner.sol
new file mode 100644
index 00000000..27f8fa63
--- /dev/null
+++ b/test/mock/ILoopringIOExchangeOwner.sol
@@ -0,0 +1,19 @@
+pragma solidity ^0.8.0;
+
+import "./IOwnable.sol";
+
+interface ILoopringIOExchangeOwner is IOwnable {
+ event SubmitBlocksAccessOpened(bool open);
+ event PermissionUpdate(address indexed user, bytes4 indexed selector, bool allowed);
+ event TargetCalled(address target, bytes data);
+ event Drained(address to, address token, uint256 amount);
+
+ function openAccessToSubmitBlocks(bool _open) external;
+ function submitBlocks(bool isDataCompressed, bytes calldata data) external;
+ function grantAccess(address user, bytes4 selector, bool granted) external;
+ function isValidSignature(bytes32 signHash, bytes memory signature) external view returns (bytes4);
+ function drain(address to, address token) external returns (uint256 amount);
+ function transferOwnership(address newOwner) external;
+ function claimOwnership() external;
+ function transact(bytes memory data) external;
+}
diff --git a/test/mock/IOwnable.sol b/test/mock/IOwnable.sol
new file mode 100644
index 00000000..d214376f
--- /dev/null
+++ b/test/mock/IOwnable.sol
@@ -0,0 +1,11 @@
+pragma solidity ^0.8.0;
+
+interface IOwnable {
+ // Event declarations
+ event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
+
+ // Function declarations
+ function transferOwnership(address newOwner) external;
+ function renounceOwnership() external;
+ function owner() external view returns (address);
+}
diff --git a/test/mock/IOwnedUpgradabilityProxy.sol b/test/mock/IOwnedUpgradabilityProxy.sol
new file mode 100644
index 00000000..2fefc629
--- /dev/null
+++ b/test/mock/IOwnedUpgradabilityProxy.sol
@@ -0,0 +1,38 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.0;
+
+interface IOwnedUpgradabilityProxy {
+ /**
+ * @dev Event to show ownership has been transferred
+ * @param previousOwner representing the address of the previous owner
+ * @param newOwner representing the address of the new owner
+ */
+ event ProxyOwnershipTransferred(address indexed previousOwner, address indexed newOwner);
+
+ /**
+ * @dev Tells the address of the owner
+ * @return The address of the owner
+ */
+ function proxyOwner() external view returns (address);
+
+ /**
+ * @dev Allows the current owner to transfer control of the contract to a newOwner.
+ * @param newOwner The address to transfer ownership to.
+ */
+ function transferProxyOwnership(address newOwner) external;
+
+ /**
+ * @dev Allows the proxy owner to upgrade the current version of the proxy.
+ * @param implementation representing the address of the new implementation to be set.
+ */
+ function upgradeTo(address implementation) external;
+
+ /**
+ * @dev Allows the proxy owner to upgrade the current version of the proxy and call the new implementation
+ * to initialize whatever is needed through a low level call.
+ * @param implementation representing the address of the new implementation to be set.
+ * @param data represents the msg.data to bet sent in the low level call. This parameter may include the function
+ * signature of the implementation to be called with the needed payload
+ */
+ function upgradeToAndCall(address implementation, bytes calldata data) external payable;
+}