diff --git a/contracts/bridge/SynapseBridge.sol b/contracts/bridge/SynapseBridge.sol index e5a896b4b..bea6e7b18 100644 --- a/contracts/bridge/SynapseBridge.sol +++ b/contracts/bridge/SynapseBridge.sol @@ -26,14 +26,26 @@ contract SynapseBridge is Initializable, AccessControlUpgradeable, ReentrancyGua bytes32 public constant NODEGROUP_ROLE = keccak256("NODEGROUP_ROLE"); bytes32 public constant GOVERNANCE_ROLE = keccak256("GOVERNANCE_ROLE"); + uint256 public constant bridgeVersion = 8; + uint256 public constant chainGasAmount = 0; + mapping(address => uint256) private fees; uint256 public startBlockNumber; - uint256 public constant bridgeVersion = 6; - uint256 public chainGasAmount; + /// @dev This is a variable taking the storage slot of deprecated chainGasAmount to prevent storage gap + uint256 private _deprecatedChainGasAmount; address payable public WETH_ADDRESS; mapping(bytes32 => bool) private kappaMap; + bool public isLegacySendDisabled; + + modifier legacySendEnabled() { + require(!isLegacySendDisabled, "Legacy send is disabled"); + _; + } + + /// @dev We add initializer modifier to constructor to prevent implementation from being initialized + constructor() public initializer {} receive() external payable {} @@ -44,8 +56,20 @@ contract SynapseBridge is Initializable, AccessControlUpgradeable, ReentrancyGua } function setChainGasAmount(uint256 amount) external { + revert("Gas airdrop is disabled"); + } + + function withdrawChainGas() external { require(hasRole(GOVERNANCE_ROLE, msg.sender), "Not governance"); - chainGasAmount = amount; + emit ChainGasWithdrawn(msg.sender, address(this).balance); + (bool success, ) = msg.sender.call{value: address(this).balance}(""); + require(success, "ETH_TRANSFER_FAILED"); + } + + function setLegacySendDisabled(bool _isLegacySendDisabled) external { + require(hasRole(GOVERNANCE_ROLE, msg.sender), "Not governance"); + isLegacySendDisabled = _isLegacySendDisabled; + emit LegacySendDisabledSet(_isLegacySendDisabled); } function setWethAddress(address payable _wethAddress) external { @@ -120,6 +144,10 @@ contract SynapseBridge is Initializable, AccessControlUpgradeable, ReentrancyGua // v2 events event TokenRedeemV2(bytes32 indexed to, uint256 chainId, IERC20 token, uint256 amount); + // New governance events + event LegacySendDisabledSet(bool isDisabled); + event ChainGasWithdrawn(address to, uint256 amount); + // VIEW FUNCTIONS ***/ function getFeeBalance(address tokenAddress) external view returns (uint256) { return fees[tokenAddress]; @@ -138,9 +166,10 @@ contract SynapseBridge is Initializable, AccessControlUpgradeable, ReentrancyGua function withdrawFees(IERC20 token, address to) external whenNotPaused { require(hasRole(GOVERNANCE_ROLE, msg.sender), "Not governance"); require(to != address(0), "Address is 0x000"); - if (fees[address(token)] != 0) { - token.safeTransfer(to, fees[address(token)]); + uint256 amount = fees[address(token)]; + if (amount != 0) { fees[address(token)] = 0; + token.safeTransfer(to, amount); } } @@ -167,7 +196,7 @@ contract SynapseBridge is Initializable, AccessControlUpgradeable, ReentrancyGua uint256 chainId, IERC20 token, uint256 amount - ) external nonReentrant whenNotPaused { + ) external nonReentrant whenNotPaused legacySendEnabled { emit TokenDeposit(to, chainId, token, amount); token.safeTransferFrom(msg.sender, address(this), amount); } @@ -184,7 +213,7 @@ contract SynapseBridge is Initializable, AccessControlUpgradeable, ReentrancyGua uint256 chainId, ERC20Burnable token, uint256 amount - ) external nonReentrant whenNotPaused { + ) external nonReentrant whenNotPaused legacySendEnabled { emit TokenRedeem(to, chainId, token, amount); token.burnFrom(msg.sender, amount); } @@ -207,6 +236,20 @@ contract SynapseBridge is Initializable, AccessControlUpgradeable, ReentrancyGua require(hasRole(NODEGROUP_ROLE, msg.sender), "Caller is not a node group"); require(amount > fee, "Amount must be greater than fee"); require(!kappaMap[kappa], "Kappa is already present"); + _withdraw(to, token, amount, fee, kappa); + } + + /** + * @dev Common internal logic for withdraw and withdrawAndRemove (once legacy workflows are disabled). + * Note: all security checks are handled outside of this function. + */ + function _withdraw( + address to, + IERC20 token, + uint256 amount, + uint256 fee, + bytes32 kappa + ) internal { kappaMap[kappa] = true; fees[address(token)] = fees[address(token)].add(fee); if (address(token) == WETH_ADDRESS && WETH_ADDRESS != address(0)) { @@ -239,14 +282,25 @@ contract SynapseBridge is Initializable, AccessControlUpgradeable, ReentrancyGua require(hasRole(NODEGROUP_ROLE, msg.sender), "Caller is not a node group"); require(amount > fee, "Amount must be greater than fee"); require(!kappaMap[kappa], "Kappa is already present"); + _mint(to, token, amount, fee, kappa); + } + + /** + * @dev Common internal logic for mint and mintAndSwap (once legacy workflows are disabled). + * Note: all security checks are handled outside of this function. + */ + function _mint( + address payable to, + IERC20Mintable token, + uint256 amount, + uint256 fee, + bytes32 kappa + ) internal { kappaMap[kappa] = true; fees[address(token)] = fees[address(token)].add(fee); emit TokenMint(to, token, amount.sub(fee), fee, kappa); token.mint(address(this), amount); IERC20(token).safeTransfer(to, amount.sub(fee)); - if (chainGasAmount != 0 && address(this).balance > chainGasAmount) { - to.call.value(chainGasAmount)(""); - } } /** @@ -269,7 +323,7 @@ contract SynapseBridge is Initializable, AccessControlUpgradeable, ReentrancyGua uint8 tokenIndexTo, uint256 minDy, uint256 deadline - ) external nonReentrant whenNotPaused { + ) external nonReentrant whenNotPaused legacySendEnabled { emit TokenDepositAndSwap(to, chainId, token, amount, tokenIndexFrom, tokenIndexTo, minDy, deadline); token.safeTransferFrom(msg.sender, address(this), amount); } @@ -294,7 +348,7 @@ contract SynapseBridge is Initializable, AccessControlUpgradeable, ReentrancyGua uint8 tokenIndexTo, uint256 minDy, uint256 deadline - ) external nonReentrant whenNotPaused { + ) external nonReentrant whenNotPaused legacySendEnabled { emit TokenRedeemAndSwap(to, chainId, token, amount, tokenIndexFrom, tokenIndexTo, minDy, deadline); token.burnFrom(msg.sender, amount); } @@ -317,7 +371,7 @@ contract SynapseBridge is Initializable, AccessControlUpgradeable, ReentrancyGua uint8 swapTokenIndex, uint256 swapMinAmount, uint256 swapDeadline - ) external nonReentrant whenNotPaused { + ) external nonReentrant whenNotPaused legacySendEnabled { emit TokenRedeemAndRemove(to, chainId, token, amount, swapTokenIndex, swapMinAmount, swapDeadline); token.burnFrom(msg.sender, amount); } @@ -351,12 +405,12 @@ contract SynapseBridge is Initializable, AccessControlUpgradeable, ReentrancyGua require(hasRole(NODEGROUP_ROLE, msg.sender), "Caller is not a node group"); require(amount > fee, "Amount must be greater than fee"); require(!kappaMap[kappa], "Kappa is already present"); + // Fallback to regular mint if legacy workflows are disabled. + if (isLegacySendDisabled) { + return _mint(to, token, amount, fee, kappa); + } kappaMap[kappa] = true; fees[address(token)] = fees[address(token)].add(fee); - // Transfer gas airdrop - if (chainGasAmount != 0 && address(this).balance > chainGasAmount) { - to.call.value(chainGasAmount)(""); - } // first check to make sure more will be given than min amount required uint256 expectedOutput = ISwap(pool).calculateSwap(tokenIndexFrom, tokenIndexTo, amount.sub(fee)); @@ -459,6 +513,10 @@ contract SynapseBridge is Initializable, AccessControlUpgradeable, ReentrancyGua require(hasRole(NODEGROUP_ROLE, msg.sender), "Caller is not a node group"); require(amount > fee, "Amount must be greater than fee"); require(!kappaMap[kappa], "Kappa is already present"); + // Fallback to regular withdraw if legacy workflows are disabled. + if (isLegacySendDisabled) { + return _withdraw(to, token, amount, fee, kappa); + } kappaMap[kappa] = true; fees[address(token)] = fees[address(token)].add(fee); // first check to make sure more will be given than min amount required @@ -526,7 +584,7 @@ contract SynapseBridge is Initializable, AccessControlUpgradeable, ReentrancyGua uint256 chainId, ERC20Burnable token, uint256 amount - ) external nonReentrant whenNotPaused { + ) external nonReentrant whenNotPaused legacySendEnabled { emit TokenRedeemV2(to, chainId, token, amount); token.burnFrom(msg.sender, amount); } diff --git a/foundry.toml b/foundry.toml index 2077f6f5c..65ce37391 100644 --- a/foundry.toml +++ b/foundry.toml @@ -52,30 +52,30 @@ op_sepolia = "${OP_SEPOLIA_API}" scroll_sepolia = "${SCROLL_SEPOLIA_API}" [etherscan] -arbitrum = { key = "${ARBITRUM_ETHERSCAN_KEY}", url = "${ARBITRUM_ETHERSCAN_URL}" } -# TODO: find out if this is correct -aurora = { key = "", url = "${AURORA_BLOCKSCOUT_URL}" } -avalanche = { key = "${AVALANCHE_ETHERSCAN_KEY}", url = "${AVALANCHE_ETHERSCAN_URL}" } -base = { key = "${BASE_ETHERSCAN_KEY}", url = "${BASE_ETHERSCAN_URL}" } -blast = { key = "${BLAST_ETHERSCAN_KEY}", url = "${BLAST_ETHERSCAN_URL}" } -boba = { key = "${BOBA_ETHERSCAN_KEY}", url = "${BOBA_ETHERSCAN_URL}" } -bsc = { key = "${BSC_ETHERSCAN_KEY}", url = "${BSC_ETHERSCAN_URL}" } -canto = { key = "", url = "${CANTO_BLOCKSCOUT_URL}" } -cronos = { key = "${CRONOS_ETHERSCAN_KEY}", url = "${CRONOS_ETHERSCAN_URL}" } +# Chains that are not currently using etherscan are commented out +arbitrum = { key = "${ARBITRUM_ETHERSCAN_KEY}", url = "${ARBITRUM_ETHERSCAN_URL}", chain = 42161 } +# aurora = { key = "", url = "${AURORA_BLOCKSCOUT_URL}" } +avalanche = { key = "${AVALANCHE_ETHERSCAN_KEY}", url = "${AVALANCHE_ETHERSCAN_URL}", chain = 43114 } +base = { key = "${BASE_ETHERSCAN_KEY}", url = "${BASE_ETHERSCAN_URL}", chain = 8453 } +blast = { key = "${BLAST_ETHERSCAN_KEY}", url = "${BLAST_ETHERSCAN_URL}", chain = 81457 } +# boba = { key = "${BOBA_ETHERSCAN_KEY}", url = "${BOBA_ETHERSCAN_URL}" } +bsc = { key = "${BSC_ETHERSCAN_KEY}", url = "${BSC_ETHERSCAN_URL}", chain = 56 } +# canto = { key = "", url = "${CANTO_BLOCKSCOUT_URL}" } +cronos = { key = "${CRONOS_ETHERSCAN_KEY}", url = "${CRONOS_ETHERSCAN_URL}", chain = 25 } # DFK is using Sourcify for verification -dogechain = { key = "", url = "${DOGECHAIN_BLOCKSCOUT_URL}" } -fantom = { key = "${FANTOM_ETHERSCAN_KEY}", url = "${FANTOM_ETHERSCAN_URL}" } +# dogechain = { key = "", url = "${DOGECHAIN_BLOCKSCOUT_URL}" } +# fantom = { key = "${FANTOM_ETHERSCAN_KEY}", url = "${FANTOM_ETHERSCAN_URL}" } # Harmony doesn't have an endpoint for verification, and Sourcify does not support Harmony # Klaytn doesn't have an endpoint for verification, and doesn't support Sourcify yet -linea = { key = "${LINEA_ETHERSCAN_KEY}", url = "${LINEA_ETHERSCAN_URL}" } -mainnet = { key = "${MAINNET_ETHERSCAN_KEY}", url = "${MAINNET_ETHERSCAN_URL}" } -metis = { key = "", url = "${METIS_BLOCKSCOUT_URL}" } -moonbeam = { key = "${MOONBEAM_ETHERSCAN_KEY}", url = "${MOONBEAM_ETHERSCAN_URL}" } -moonriver = { key = "${MOONRIVER_ETHERSCAN_KEY}", url = "${MOONRIVER_ETHERSCAN_URL}" } -optimism = { key = "${OPTIMISM_ETHERSCAN_KEY}", url = "${OPTIMISM_ETHERSCAN_URL}" } -polygon = { key = "${POLYGON_ETHERSCAN_KEY}", url = "${POLYGON_ETHERSCAN_URL}" } -scroll = { key = "${SCROLL_ETHERSCAN_KEY}", url = "${SCROLL_ETHERSCAN_URL}" } -zkevm = { key = "${ZKEVM_ETHERSCAN_KEY}", url = "${ZKEVM_ETHERSCAN_URL}" } +linea = { key = "${LINEA_ETHERSCAN_KEY}", url = "${LINEA_ETHERSCAN_URL}", chain = 59144 } +mainnet = { key = "${MAINNET_ETHERSCAN_KEY}", url = "${MAINNET_ETHERSCAN_URL}", chain = 1 } +# metis = { key = "", url = "${METIS_BLOCKSCOUT_URL}" } +moonbeam = { key = "${MOONBEAM_ETHERSCAN_KEY}", url = "${MOONBEAM_ETHERSCAN_URL}", chain = 1284 } +moonriver = { key = "${MOONRIVER_ETHERSCAN_KEY}", url = "${MOONRIVER_ETHERSCAN_URL}", chain = 1285 } +optimism = { key = "${OPTIMISM_ETHERSCAN_KEY}", url = "${OPTIMISM_ETHERSCAN_URL}", chain = 10 } +polygon = { key = "${POLYGON_ETHERSCAN_KEY}", url = "${POLYGON_ETHERSCAN_URL}", chain = 137 } +scroll = { key = "${SCROLL_ETHERSCAN_KEY}", url = "${SCROLL_ETHERSCAN_URL}", chain = 534532 } +zkevm = { key = "${ZKEVM_ETHERSCAN_KEY}", url = "${ZKEVM_ETHERSCAN_URL}", chain = 1101 } # Testnets arb_sepolia = { key = "${ARB_SEPOLIA_ETHERSCAN_KEY}", url = "${ARB_SEPOLIA_ETHERSCAN_URL}" } base_sepolia = { key = "${BASE_SEPOLIA_ETHERSCAN_KEY}", url = "${BASE_SEPOLIA_ETHERSCAN_URL}" } diff --git a/script/bridge/DeploySynapseBridge.s.sol b/script/bridge/DeploySynapseBridge.s.sol new file mode 100644 index 000000000..088fb5207 --- /dev/null +++ b/script/bridge/DeploySynapseBridge.s.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.6.12; +pragma experimental ABIEncoderV2; + +import {SynapseBridge} from "../../contracts/bridge/SynapseBridge.sol"; + +import {BasicSynapseScript, StringUtils} from "../templates/BasicSynapse.s.sol"; + +contract DeploySynapseBridge is BasicSynapseScript { + using StringUtils for string; + + // TODO: mine a create2 salt for this + bytes32 internal salt = 0; + + function run() external { + // Setup the BasicSynapseScript + setUp(); + address bridge = tryGetDeploymentAddress("SynapseBridge"); + if (bridge == address(0)) { + printLog(StringUtils.concat("🟡 Skipping: SynapseBridge is not deployed on ", activeChain)); + return; + } + vm.startBroadcast(); + address predicted = predictAddress(type(SynapseBridge).creationCode, salt); + printLog(StringUtils.concat("Predicted address: ", vm.toString(predicted))); + address deployed = deployAndSaveAs({ + contractName: "SynapseBridge", + contractAlias: "SynapseBridge.Implementation", + constructorArgs: "", + deployCode: deployCreate2 + }); + if (predicted != deployed) { + printLog(TAB.concat("❌ Predicted address mismatch")); + assert(false); + } + vm.stopBroadcast(); + } +} diff --git a/script/bridge/deploy-implementation.sh b/script/bridge/deploy-implementation.sh new file mode 100755 index 000000000..d1eaaa588 --- /dev/null +++ b/script/bridge/deploy-implementation.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# This script deploys the SynapseBridge implementation on all chains +# Usage: ./script/bridge/deploy-implementation.sh [] +# - name of the wallet to use for deployment + +# Colors +RED="\033[0;31m" +NC="\033[0m" # No Color + +WALLET_NAME=$1 +# Get the rest of the args +shift 1 +# Check that all required args exist +if [ -z "$WALLET_NAME" ]; then + echo -e "${RED}Usage: ./script/bridge/deploy-implementation.sh []${NC}" + exit 1 +fi + +# Make sure the script is run from the root of the project +PROJECT_ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")"/../../ && pwd) +cd "$PROJECT_ROOT" || exit 1 + +# Run the script on all chains with SynapseBridge deployment +# Look within deployments/chainName for SynapseBridge.json +for chainName in $(ls deployments); do + if [ -f "deployments/$chainName/SynapseBridge.json" ]; then + ./script/run.sh ./script/bridge/DeploySynapseBridge.s.sol "$chainName" "$WALLET_NAME" "$@" + fi +done diff --git a/script/templates/BasicUtils.sol b/script/templates/BasicUtils.sol index 5457dc763..0fc169f86 100644 --- a/script/templates/BasicUtils.sol +++ b/script/templates/BasicUtils.sol @@ -275,11 +275,11 @@ abstract contract BasicUtils is CommonBase { string memory pathOutput, string memory key ) internal returns (string memory fullInputData) { - // Example: jq .abi=$data.abi --argfile data path/to/input.json path/to/output.json + // Example: jq .abi=$data[0].abi --slurpfile data path/to/input.json path/to/output.json string[] memory inputs = new string[](6); inputs[0] = "jq"; - inputs[1] = key.concat(" = $data", key); - inputs[2] = "--argfile"; + inputs[1] = key.concat(" = $data[0]", key); + inputs[2] = "--slurpfile"; inputs[3] = "data"; inputs[4] = pathInput; inputs[5] = pathOutput; diff --git a/test/bridge/legacy/PoolMock.sol b/test/bridge/legacy/PoolMock.sol new file mode 100644 index 000000000..9d6928f4f --- /dev/null +++ b/test/bridge/legacy/PoolMock.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.6.12; + +// solhint-disable no-empty-blocks, no-unused-vars +contract PoolMock { + /// @notice We include an empty "test" function so that this contract does not appear in the coverage report. + function testPoolMock() external {} + + function calculateSwap( + uint8 tokenIndexFrom, + uint8 tokenIndexTo, + uint256 amount + ) external returns (uint256) {} + + function calculateRemoveLiquidityOneToken(uint256 tokenAmount, uint8 tokenIndex) external returns (uint256) {} +} diff --git a/test/bridge/legacy/ReenteringToken.sol b/test/bridge/legacy/ReenteringToken.sol new file mode 100644 index 000000000..6d1b15718 --- /dev/null +++ b/test/bridge/legacy/ReenteringToken.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.6.12; +pragma experimental ABIEncoderV2; + +import {SynapseERC20} from "../../../contracts/bridge/SynapseERC20.sol"; + +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; + +contract ReenteringToken is SynapseERC20 { + address internal target; + bytes internal data; + + function setReenteringData(address target_, bytes memory data_) public { + target = target_; + data = data_; + } + + function _transfer( + address sender, + address recipient, + uint256 amount + ) internal virtual override { + super._transfer(sender, recipient, amount); + _reenterTarget(); + } + + function _mint(address account, uint256 amount) internal virtual override { + super._mint(account, amount); + _reenterTarget(); + } + + function _reenterTarget() internal { + if (target != address(0)) { + address target_ = target; + bytes memory data_ = data; + delete target; + delete data; + Address.functionCall(target_, data_); + } + } +} diff --git a/test/bridge/legacy/SynapseBridge.Dst.t.sol b/test/bridge/legacy/SynapseBridge.Dst.t.sol new file mode 100644 index 000000000..a7295eca1 --- /dev/null +++ b/test/bridge/legacy/SynapseBridge.Dst.t.sol @@ -0,0 +1,337 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.6.12; +pragma experimental ABIEncoderV2; + +import {SynapseBridge, IERC20, ERC20Burnable, IERC20Mintable, ISwap} from "../../../contracts/bridge/SynapseBridge.sol"; + +import {ReenteringToken} from "./ReenteringToken.sol"; +import {PoolMock} from "./PoolMock.sol"; +import {SynapseBridgeProxyTest} from "./SynapseBridge.Proxy.t.sol"; + +// solhint-disable func-name-mixedcase +contract SynapseBridgeLegacyDstTest is SynapseBridgeProxyTest { + ReenteringToken internal token; + address internal pool; + + address internal nodeGroup = makeAddr("NodeGroup"); + address internal user = makeAddr("User"); + bytes32 internal kappa = keccak256("kappa"); + + uint256 internal initialLockedBalance = 1000 ether; + uint256 internal amount = 1 ether; + uint256 internal fee = 0.01 ether; + + event TokenWithdraw(address indexed to, address token, uint256 amount, uint256 fee, bytes32 indexed kappa); + event TokenMint(address indexed to, address token, uint256 amount, uint256 fee, bytes32 indexed kappa); + event TokenMintAndSwap( + address indexed to, + address token, + uint256 amount, + uint256 fee, + uint8 tokenIndexFrom, + uint8 tokenIndexTo, + uint256 minDy, + uint256 deadline, + bool swapSuccess, + bytes32 indexed kappa + ); + event TokenWithdrawAndRemove( + address indexed to, + address token, + uint256 amount, + uint256 fee, + uint8 swapTokenIndex, + uint256 swapMinAmount, + uint256 swapDeadline, + bool swapSuccess, + bytes32 indexed kappa + ); + + event TokenDeposit(address indexed to, uint256 chainId, address token, uint256 amount); + + modifier withMintToken() { + token.grantRole(token.MINTER_ROLE(), address(bridge)); + _; + } + + modifier withWithdrawToken() { + deal(address(token), address(bridge), initialLockedBalance, true); + _; + } + + modifier withRevertingPool() { + vm.mockCallRevert({callee: address(pool), data: "", revertData: "GM"}); + _; + } + + modifier withLegacySendDisabled() { + bridge.setLegacySendDisabled(true); + _; + } + + function prepareReentrancyData() public { + token.setReenteringData( + address(bridge), + abi.encodeWithSelector(bridge.deposit.selector, address(user), 1, address(token), 0) + ); + } + + function expectMintEvent() public { + vm.expectEmit(address(bridge)); + emit TokenMint(user, address(token), amount - fee, fee, kappa); + } + + function expectMintAndSwapEvent() public { + vm.expectEmit(address(bridge)); + emit TokenMintAndSwap(user, address(token), amount - fee, fee, 0, 0, 1, 0, false, kappa); + } + + function expectWithdrawEvent() public { + // Note: TokenWithdraw emits amount before fees, which is left as is to preserve legacy behavior + vm.expectEmit(address(bridge)); + emit TokenWithdraw(user, address(token), amount, fee, kappa); + } + + function expectWithdrawAndRemoveEvent() public { + vm.expectEmit(address(bridge)); + emit TokenWithdrawAndRemove(user, address(token), amount - fee, fee, 0, 1, 0, false, kappa); + } + + function expectReentrancyDepositEvent() public { + vm.expectEmit(address(bridge)); + emit TokenDeposit(user, 1, address(token), 0); + } + + function expectBalances( + uint256 bridgeBalance, + uint256 userBalance, + uint256 bridgeFees + ) public { + assertEq(token.balanceOf(address(bridge)), bridgeBalance); + assertEq(token.balanceOf(user), userBalance); + assertEq(bridge.getFeeBalance(address(token)), bridgeFees); + } + + function setUp() public virtual override { + super.setUp(); + bridge.initialize(); + bridge.grantRole(bridge.GOVERNANCE_ROLE(), address(this)); + bridge.grantRole(bridge.NODEGROUP_ROLE(), nodeGroup); + + token = new ReenteringToken(); + token.initialize("Test", "TST", 18, address(this)); + + pool = address(new PoolMock()); + } + + function test_mint() public withMintToken { + expectMintEvent(); + vm.prank(nodeGroup); + bridge.mint(payable(user), IERC20Mintable(address(token)), amount, fee, kappa); + expectBalances({bridgeBalance: fee, userBalance: amount - fee, bridgeFees: fee}); + assertTrue(bridge.kappaExists(kappa)); + } + + function test_mintAndSwap() public withMintToken { + expectMintAndSwapEvent(); + vm.prank(nodeGroup); + // Use minDy > 0 so that it exceeds calculateSwap = 0 (swapSuccess = false). + bridge.mintAndSwap(payable(user), IERC20Mintable(address(token)), amount, fee, ISwap(pool), 0, 0, 1, 0, kappa); + expectBalances({bridgeBalance: fee, userBalance: amount - fee, bridgeFees: fee}); + assertTrue(bridge.kappaExists(kappa)); + } + + function test_mintAndSwap_reverts_withRevertingPool() public withMintToken withRevertingPool { + vm.expectRevert(bytes("GM")); + vm.prank(nodeGroup); + bridge.mintAndSwap(payable(user), IERC20Mintable(address(token)), amount, fee, ISwap(pool), 0, 0, 1, 0, kappa); + } + + /// @notice Should behave the same way as the legacy workflow. + function test_mint_legacySendDisabled() public withLegacySendDisabled { + test_mint(); + } + + /// @notice Should ignore andSwap instructions in legacySendDisabled mode. + function test_mintAndSwap_legacySendDisabled_mintFallback() public withLegacySendDisabled withMintToken { + expectMintEvent(); + vm.prank(nodeGroup); + bridge.mintAndSwap(payable(user), IERC20Mintable(address(token)), amount, fee, ISwap(pool), 0, 0, 1, 0, kappa); + expectBalances({bridgeBalance: fee, userBalance: amount - fee, bridgeFees: fee}); + assertTrue(bridge.kappaExists(kappa)); + } + + /// @notice Pool is never called in legacySendDisabled mode, so should be identical to mint fallback. + function test_mintAndSwap_legacySendDisabled_mintFallback_withRevertingPool() public withRevertingPool { + test_mintAndSwap_legacySendDisabled_mintFallback(); + } + + function test_withdraw() public withWithdrawToken { + expectWithdrawEvent(); + vm.prank(nodeGroup); + bridge.withdraw(payable(user), IERC20(address(token)), amount, fee, kappa); + expectBalances({ + bridgeBalance: initialLockedBalance - amount + fee, + userBalance: amount - fee, + bridgeFees: fee + }); + assertTrue(bridge.kappaExists(kappa)); + } + + function test_withdrawAndRemove() public withWithdrawToken { + expectWithdrawAndRemoveEvent(); + vm.prank(nodeGroup); + // Use swapMinAmount > 0 so that it exceeds calculateRemoveLiquidityOneToken = 0 (swapSuccess = false). + bridge.withdrawAndRemove(payable(user), IERC20(address(token)), amount, fee, ISwap(pool), 0, 1, 0, kappa); + expectBalances({ + bridgeBalance: initialLockedBalance - amount + fee, + userBalance: amount - fee, + bridgeFees: fee + }); + assertTrue(bridge.kappaExists(kappa)); + } + + function test_withdrawAndRemove_reverts_withRevertingPool() public withRevertingPool { + vm.expectRevert(bytes("GM")); + vm.prank(nodeGroup); + bridge.withdrawAndRemove(payable(user), IERC20(address(token)), amount, fee, ISwap(pool), 0, 1, 0, kappa); + } + + /// @notice Should behave the same way as the legacy workflow. + function test_withdraw_legacySendDisabled() public withLegacySendDisabled { + test_withdraw(); + } + + /// @notice Should ignore andRemove instructions in legacySendDisabled mode. + function test_withdrawAndRemove_legacySendDisabled_withdrawFallback() + public + withLegacySendDisabled + withWithdrawToken + { + expectWithdrawEvent(); + vm.prank(nodeGroup); + bridge.withdrawAndRemove(payable(user), IERC20(address(token)), amount, fee, ISwap(pool), 0, 1, 0, kappa); + expectBalances({ + bridgeBalance: initialLockedBalance - amount + fee, + userBalance: amount - fee, + bridgeFees: fee + }); + assertTrue(bridge.kappaExists(kappa)); + } + + /// @notice Pool is never called in legacySendDisabled mode, so should be identical to withdraw fallback. + function test_withdrawAndRemove_legacySendDisabled_withdrawFallback_withRevertingPool() public withRevertingPool { + test_withdrawAndRemove_legacySendDisabled_withdrawFallback(); + } + + // ═════════════════════════════════════════════ TESTS: REENTRANCY ═════════════════════════════════════════════════ + + function test_setupReentrancyMint() public withMintToken { + token.grantRole(token.MINTER_ROLE(), address(this)); + prepareReentrancyData(); + expectReentrancyDepositEvent(); + token.mint(address(user), 0); + } + + function test_setupReentrancyTransfer() public withWithdrawToken { + prepareReentrancyData(); + expectReentrancyDepositEvent(); + token.transfer(address(user), 0); + } + + function test_mint_reverts_withReentrancy() public withMintToken { + prepareReentrancyData(); + vm.expectRevert("ReentrancyGuard: reentrant call"); + vm.prank(nodeGroup); + bridge.mint(payable(user), IERC20Mintable(address(token)), amount, fee, kappa); + } + + function test_mintAndSwap_reverts_withReentrancy() public withMintToken { + prepareReentrancyData(); + vm.expectRevert("ReentrancyGuard: reentrant call"); + vm.prank(nodeGroup); + bridge.mintAndSwap(payable(user), IERC20Mintable(address(token)), amount, fee, ISwap(pool), 0, 0, 1, 0, kappa); + } + + function test_mint_legacySendDisabled_reverts_withReentrancy() public withLegacySendDisabled { + test_mint_reverts_withReentrancy(); + } + + function test_mintAndSwap_legacySendDisabled_reverts_withReentrancy() public withLegacySendDisabled { + test_mintAndSwap_reverts_withReentrancy(); + } + + function test_withdraw_reverts_withReentrancy() public withWithdrawToken { + prepareReentrancyData(); + vm.expectRevert("ReentrancyGuard: reentrant call"); + vm.prank(nodeGroup); + bridge.withdraw(payable(user), IERC20(address(token)), amount, fee, kappa); + } + + function test_withdrawAndRemove_reverts_withReentrancy() public withWithdrawToken { + prepareReentrancyData(); + vm.expectRevert("ReentrancyGuard: reentrant call"); + vm.prank(nodeGroup); + bridge.withdrawAndRemove(payable(user), IERC20(address(token)), amount, fee, ISwap(pool), 0, 1, 0, kappa); + } + + function test_withdraw_legacySendDisabled_reverts_withReentrancy() public withLegacySendDisabled { + test_withdraw_reverts_withReentrancy(); + } + + function test_withdrawAndRemove_legacySendDisabled_reverts_withReentrancy() public withLegacySendDisabled { + test_withdrawAndRemove_reverts_withReentrancy(); + } + + // ══════════════════════════════════════════ TESTS: NODE GROUP ONLY ═══════════════════════════════════════════════ + + function test_mint_reverts_callerNotNodeGroup(address caller) public withMintToken { + vm.assume(caller != nodeGroup); + vm.prank(caller); + vm.expectRevert("Caller is not a node group"); + bridge.mint(payable(user), IERC20Mintable(address(token)), amount, fee, kappa); + } + + function test_mintAndSwap_reverts_callerNotNodeGroup(address caller) public withMintToken { + vm.assume(caller != nodeGroup); + vm.prank(caller); + vm.expectRevert("Caller is not a node group"); + bridge.mintAndSwap(payable(user), IERC20Mintable(address(token)), amount, fee, ISwap(pool), 0, 0, 1, 0, kappa); + } + + function test_mint_legacySendDisabled_reverts_callerNotNodeGroup(address caller) public withLegacySendDisabled { + test_mint_reverts_callerNotNodeGroup(caller); + } + + function test_mintAndSwap_legacySendDisabled_reverts_callerNotNodeGroup(address caller) + public + withLegacySendDisabled + { + test_mintAndSwap_reverts_callerNotNodeGroup(caller); + } + + function test_withdraw_reverts_callerNotNodeGroup(address caller) public withWithdrawToken { + vm.assume(caller != nodeGroup); + vm.prank(caller); + vm.expectRevert("Caller is not a node group"); + bridge.withdraw(payable(user), IERC20(address(token)), amount, fee, kappa); + } + + function test_withdrawAndRemove_reverts_callerNotNodeGroup(address caller) public withWithdrawToken { + vm.assume(caller != nodeGroup); + vm.prank(caller); + vm.expectRevert("Caller is not a node group"); + bridge.withdrawAndRemove(payable(user), IERC20(address(token)), amount, fee, ISwap(pool), 0, 1, 0, kappa); + } + + function test_withdraw_legacySendDisabled_reverts_callerNotNodeGroup(address caller) public withLegacySendDisabled { + test_withdraw_reverts_callerNotNodeGroup(caller); + } + + function test_withdrawAndRemove_legacySendDisabled_reverts_callerNotNodeGroup(address caller) + public + withLegacySendDisabled + { + test_withdrawAndRemove_reverts_callerNotNodeGroup(caller); + } +} diff --git a/test/bridge/legacy/SynapseBridge.Fees.t.sol b/test/bridge/legacy/SynapseBridge.Fees.t.sol new file mode 100644 index 000000000..6739dd374 --- /dev/null +++ b/test/bridge/legacy/SynapseBridge.Fees.t.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.6.12; +pragma experimental ABIEncoderV2; + +import {SynapseBridge, IERC20} from "../../../contracts/bridge/SynapseBridge.sol"; +import {ReenteringToken} from "./ReenteringToken.sol"; +import {SynapseBridgeProxyTest} from "./SynapseBridge.Proxy.t.sol"; + +contract Governance { + function withdrawFees(SynapseBridge bridge, address token) public { + bridge.withdrawFees(IERC20(token), address(this)); + } +} + +// solhint-disable func-name-mixedcase +contract SynapseBridgeLegacyFeesTest is SynapseBridgeProxyTest { + ReenteringToken internal token; + Governance internal governance; + + uint256 internal feesAmount = 1 ether; + uint256 internal lockedAmount = 10 ether; + + function setUp() public virtual override { + super.setUp(); + bridge.initialize(); + governance = new Governance(); + bridge.grantRole(bridge.GOVERNANCE_ROLE(), address(governance)); + bridge.grantRole(bridge.NODEGROUP_ROLE(), address(this)); + + token = new ReenteringToken(); + token.initialize("Test", "TST", 18, address(this)); + token.grantRole(token.MINTER_ROLE(), address(this)); + + token.mint(address(bridge), lockedAmount); + // Withdraw to self to set up the fees without moving any tokens + bridge.withdraw(address(bridge), IERC20(address(token)), lockedAmount, feesAmount, 0); + } + + function test_withdrawFees() public { + governance.withdrawFees(bridge, address(token)); + assertEq(token.balanceOf(address(governance)), feesAmount); + assertEq(token.balanceOf(address(bridge)), lockedAmount - feesAmount); + assertEq(bridge.getFeeBalance(address(token)), 0); + } + + function test_withdrawFees_reentrancy() public { + // Governance reenters withdrawFees after receiving the fees + token.setReenteringData( + address(governance), + abi.encodeWithSelector(Governance.withdrawFees.selector, bridge, token) + ); + // Second withdrawFees should be with 0 fees, so exact same end state + test_withdrawFees(); + } + + function test_withdrawFees_reverts_notGovernance(address caller) public { + vm.assume(caller != address(governance)); + vm.prank(caller); + vm.expectRevert("Not governance"); + bridge.withdrawFees(IERC20(address(token)), address(1)); + } + + function test_withdrawFees_reverts_zeroRecipient() public { + vm.prank(address(governance)); + vm.expectRevert("Address is 0x000"); + bridge.withdrawFees(IERC20(address(token)), address(0)); + } +} diff --git a/test/bridge/legacy/SynapseBridge.Proxy.t.sol b/test/bridge/legacy/SynapseBridge.Proxy.t.sol new file mode 100644 index 000000000..9b6dc73a7 --- /dev/null +++ b/test/bridge/legacy/SynapseBridge.Proxy.t.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.6.12; +pragma experimental ABIEncoderV2; + +import {Test} from "forge-std/Test.sol"; + +import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; +import {SynapseBridge} from "../../../contracts/bridge/SynapseBridge.sol"; + +// solhint-disable func-name-mixedcase +abstract contract SynapseBridgeProxyTest is Test { + SynapseBridge internal bridge; + address internal implementation; + + function setUp() public virtual { + implementation = address(new SynapseBridge()); + // Tests don't need to assume the exact proxy structure, so we can use a minimal proxy. + bridge = SynapseBridge(payable(Clones.clone(implementation))); + } + + function test_initialize_implementation_revert() public { + vm.expectRevert("Initializable: contract is already initialized"); + SynapseBridge(payable(implementation)).initialize(); + } +} diff --git a/test/bridge/legacy/SynapseBridge.Upgrade.t.sol b/test/bridge/legacy/SynapseBridge.Upgrade.t.sol new file mode 100644 index 000000000..c9211be54 --- /dev/null +++ b/test/bridge/legacy/SynapseBridge.Upgrade.t.sol @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.6.12; +pragma experimental ABIEncoderV2; + +import {SynapseBridge} from "../../../contracts/bridge/SynapseBridge.sol"; + +import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/TransparentUpgradeableProxy.sol"; +import {Test} from "forge-std/Test.sol"; + +// solhint-disable func-name-mixedcase +// TODO: rename to TestFork to remove this from CI workflow post-migration +contract SynapseBridgeUpgradeArbitrumTest is Test { + // 2025-06-05 + uint256 internal blockNumber = 344210000; + address payable internal bridge = 0x6F4e8eBa4D337f874Ab57478AcC2Cb5BACdc19c9; + address internal proxyAdmin = 0x432036208d2717394d2614d6697c46DF3Ed69540; + + uint256 internal expectedBridgeVersion = 6; + uint256 internal expectedChainGasAmount = 0.00001 ether; + string internal rpcUrl = "https://arbitrum-one.public.blastapi.io"; + + address internal newBridgeImplementation; + + address internal token = 0xfc5A1A6EB076a2C7aD06eD22C90d7E710E35ad0a; + uint256 internal existingFees; + bytes32 internal kappa = 0xbdecec2b295b7ed28c44d9849c7dbf773516cbe0c219df850a10c4bcc6bbb7dd; + + address internal roleAdmin; + address internal governance; + address internal nodeGroup; + + function setUp() public { + vm.createSelectFork(rpcUrl, blockNumber); + newBridgeImplementation = address(new SynapseBridge()); + + existingFees = SynapseBridge(bridge).getFeeBalance(token); + roleAdmin = SynapseBridge(bridge).getRoleMember(SynapseBridge(bridge).DEFAULT_ADMIN_ROLE(), 0); + governance = SynapseBridge(bridge).getRoleMember(SynapseBridge(bridge).GOVERNANCE_ROLE(), 0); + nodeGroup = SynapseBridge(bridge).getRoleMember(SynapseBridge(bridge).NODEGROUP_ROLE(), 0); + } + + function upgrade() public { + vm.prank(proxyAdmin); + TransparentUpgradeableProxy(bridge).upgradeTo(newBridgeImplementation); + } + + function test_getters() public { + assertEq(SynapseBridge(bridge).getRoleMember(SynapseBridge(bridge).DEFAULT_ADMIN_ROLE(), 0), roleAdmin); + assertEq(SynapseBridge(bridge).getRoleMember(SynapseBridge(bridge).GOVERNANCE_ROLE(), 0), governance); + assertEq(SynapseBridge(bridge).getRoleMember(SynapseBridge(bridge).NODEGROUP_ROLE(), 0), nodeGroup); + + assertEq(SynapseBridge(bridge).bridgeVersion(), expectedBridgeVersion); + assertEq(SynapseBridge(bridge).chainGasAmount(), expectedChainGasAmount); + + assertEq(SynapseBridge(bridge).getFeeBalance(token), existingFees); + assertGt(existingFees, 0); + + assertTrue(SynapseBridge(bridge).kappaExists(kappa)); + assertFalse(SynapseBridge(bridge).kappaExists(kappa ^ bytes32(uint256(1)))); + } + + function test_upgrade() public { + upgrade(); + expectedBridgeVersion = 8; + expectedChainGasAmount = 0; + test_getters(); + assertFalse(SynapseBridge(bridge).isLegacySendDisabled()); + } +} diff --git a/test/bridge/legacy/SynapseBridge.t.sol b/test/bridge/legacy/SynapseBridge.t.sol new file mode 100644 index 000000000..4cca95334 --- /dev/null +++ b/test/bridge/legacy/SynapseBridge.t.sol @@ -0,0 +1,324 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.6.12; +pragma experimental ABIEncoderV2; + +import {SynapseBridge, IERC20, ERC20Burnable} from "../../../contracts/bridge/SynapseBridge.sol"; +import {SynapseERC20} from "../../../contracts/bridge/SynapseERC20.sol"; + +import {SynapseBridgeProxyTest} from "./SynapseBridge.Proxy.t.sol"; + +// solhint-disable func-name-mixedcase +contract SynapseBridgeLegacyTest is SynapseBridgeProxyTest { + SynapseERC20 internal token; + + address internal user = makeAddr("User"); + address internal governance = makeAddr("Governance"); + + event TokenDeposit(address indexed to, uint256 chainId, address token, uint256 amount); + event TokenRedeem(address indexed to, uint256 chainId, address token, uint256 amount); + event TokenDepositAndSwap( + address indexed to, + uint256 chainId, + address token, + uint256 amount, + uint8 tokenIndexFrom, + uint8 tokenIndexTo, + uint256 minDy, + uint256 deadline + ); + event TokenRedeemAndSwap( + address indexed to, + uint256 chainId, + address token, + uint256 amount, + uint8 tokenIndexFrom, + uint8 tokenIndexTo, + uint256 minDy, + uint256 deadline + ); + event TokenRedeemAndRemove( + address indexed to, + uint256 chainId, + address token, + uint256 amount, + uint8 swapTokenIndex, + uint256 swapMinAmount, + uint256 swapDeadline + ); + + event TokenRedeemV2(bytes32 indexed to, uint256 chainId, address token, uint256 amount); + + event LegacySendDisabledSet(bool isDisabled); + event ChainGasWithdrawn(address to, uint256 amount); + + function setUp() public virtual override { + super.setUp(); + bridge.initialize(); + bridge.grantRole(bridge.GOVERNANCE_ROLE(), governance); + + token = new SynapseERC20(); + token.initialize("Test", "TST", 18, address(this)); + token.grantRole(token.MINTER_ROLE(), address(this)); + + token.mint(user, 1 ether); + vm.prank(user); + token.approve(address(bridge), type(uint256).max); + } + + function disableLegacySend() public { + vm.prank(governance); + bridge.setLegacySendDisabled(true); + } + + function enableLegacySend() public { + vm.prank(governance); + bridge.setLegacySendDisabled(false); + } + + function test_disableLegacySend() public { + vm.expectEmit(address(bridge)); + emit LegacySendDisabledSet(true); + disableLegacySend(); + assertTrue(bridge.isLegacySendDisabled()); + } + + function test_enableLegacySend() public { + disableLegacySend(); + vm.expectEmit(address(bridge)); + emit LegacySendDisabledSet(false); + enableLegacySend(); + assertFalse(bridge.isLegacySendDisabled()); + } + + function test_setLegacySendDisabled_revert_notGovernance(address caller) public { + vm.assume(caller != governance); + vm.prank(caller); + vm.expectRevert("Not governance"); + bridge.setLegacySendDisabled(true); + } + + function test_withdrawChainGas() public { + deal(address(bridge), 123456); + vm.expectEmit(address(bridge)); + emit ChainGasWithdrawn(governance, 123456); + vm.prank(governance); + bridge.withdrawChainGas(); + assertEq(governance.balance, 123456); + } + + function test_withdrawChainGas_revert_notGovernance(address caller) public { + deal(address(bridge), 123456); + vm.assume(caller != governance); + vm.prank(caller); + vm.expectRevert("Not governance"); + bridge.withdrawChainGas(); + } + + function test_setChainGasAmount_revertsAnyCaller(address caller) public { + vm.expectRevert("Gas airdrop is disabled"); + vm.prank(caller); + bridge.setChainGasAmount(123456); + } + + function test_setChainGasAmount_revertsGovernance() public { + test_setChainGasAmount_revertsAnyCaller(governance); + } + + function test_deposit() public { + vm.expectEmit(address(bridge)); + emit TokenDeposit({to: address(1), chainId: 2, token: address(token), amount: 3}); + vm.prank(user); + bridge.deposit({to: address(1), chainId: 2, token: IERC20(address(token)), amount: 3}); + } + + function test_deposit_reenabled() public { + disableLegacySend(); + enableLegacySend(); + test_deposit(); + } + + function test_deposit_revert_disabled() public { + disableLegacySend(); + vm.expectRevert("Legacy send is disabled"); + vm.prank(user); + bridge.deposit({to: address(1), chainId: 2, token: IERC20(address(token)), amount: 3}); + } + + function test_depositAndSwap() public { + vm.expectEmit(address(bridge)); + emit TokenDepositAndSwap({ + to: address(1), + chainId: 2, + token: address(token), + amount: 3, + tokenIndexFrom: 4, + tokenIndexTo: 5, + minDy: 6, + deadline: 7 + }); + vm.prank(user); + bridge.depositAndSwap({ + to: address(1), + chainId: 2, + token: IERC20(address(token)), + amount: 3, + tokenIndexFrom: 4, + tokenIndexTo: 5, + minDy: 6, + deadline: 7 + }); + } + + function test_depositAndSwap_reenabled() public { + disableLegacySend(); + enableLegacySend(); + test_depositAndSwap(); + } + + function test_depositAndSwap_revert_disabled() public { + disableLegacySend(); + vm.expectRevert("Legacy send is disabled"); + vm.prank(user); + bridge.depositAndSwap({ + to: address(1), + chainId: 2, + token: IERC20(address(token)), + amount: 3, + tokenIndexFrom: 4, + tokenIndexTo: 5, + minDy: 6, + deadline: 7 + }); + } + + function test_redeem() public { + vm.expectEmit(address(bridge)); + emit TokenRedeem({to: address(1), chainId: 2, token: address(token), amount: 3}); + vm.prank(user); + bridge.redeem({to: address(1), chainId: 2, token: ERC20Burnable(address(token)), amount: 3}); + } + + function test_redeem_reenabled() public { + disableLegacySend(); + enableLegacySend(); + test_redeem(); + } + + function test_redeem_revert_disabled() public { + disableLegacySend(); + vm.expectRevert("Legacy send is disabled"); + vm.prank(user); + bridge.redeem({to: address(1), chainId: 2, token: ERC20Burnable(address(token)), amount: 3}); + } + + function test_redeemAndSwap() public { + vm.expectEmit(address(bridge)); + emit TokenRedeemAndSwap({ + to: address(1), + chainId: 2, + token: address(token), + amount: 3, + tokenIndexFrom: 4, + tokenIndexTo: 5, + minDy: 6, + deadline: 7 + }); + vm.prank(user); + bridge.redeemAndSwap({ + to: address(1), + chainId: 2, + token: ERC20Burnable(address(token)), + amount: 3, + tokenIndexFrom: 4, + tokenIndexTo: 5, + minDy: 6, + deadline: 7 + }); + } + + function test_redeemAndSwap_reenabled() public { + disableLegacySend(); + enableLegacySend(); + test_redeemAndSwap(); + } + + function test_redeemAndSwap_revert_disabled() public { + disableLegacySend(); + vm.expectRevert("Legacy send is disabled"); + vm.prank(user); + bridge.redeemAndSwap({ + to: address(1), + chainId: 2, + token: ERC20Burnable(address(token)), + amount: 3, + tokenIndexFrom: 4, + tokenIndexTo: 5, + minDy: 6, + deadline: 7 + }); + } + + function test_redeemAndRemove() public { + vm.expectEmit(address(bridge)); + emit TokenRedeemAndRemove({ + to: address(1), + chainId: 2, + token: address(token), + amount: 3, + swapTokenIndex: 4, + swapMinAmount: 5, + swapDeadline: 6 + }); + vm.prank(user); + bridge.redeemAndRemove({ + to: address(1), + chainId: 2, + token: ERC20Burnable(address(token)), + amount: 3, + swapTokenIndex: 4, + swapMinAmount: 5, + swapDeadline: 6 + }); + } + + function test_redeemAndRemove_reenabled() public { + disableLegacySend(); + enableLegacySend(); + test_redeemAndRemove(); + } + + function test_redeemAndRemove_revert_disabled() public { + disableLegacySend(); + vm.expectRevert("Legacy send is disabled"); + vm.prank(user); + bridge.redeemAndRemove({ + to: address(1), + chainId: 2, + token: ERC20Burnable(address(token)), + amount: 3, + swapTokenIndex: 4, + swapMinAmount: 5, + swapDeadline: 6 + }); + } + + function test_redeemV2() public { + vm.expectEmit(address(bridge)); + emit TokenRedeemV2({to: bytes32(uint256(1)), chainId: 2, token: address(token), amount: 3}); + vm.prank(user); + bridge.redeemV2({to: bytes32(uint256(1)), chainId: 2, token: ERC20Burnable(address(token)), amount: 3}); + } + + function test_redeemV2_reenabled() public { + disableLegacySend(); + enableLegacySend(); + test_redeemV2(); + } + + function test_redeemV2_revert_disabled() public { + disableLegacySend(); + vm.expectRevert("Legacy send is disabled"); + vm.prank(user); + bridge.redeemV2({to: bytes32(uint256(1)), chainId: 2, token: ERC20Burnable(address(token)), amount: 3}); + } +} diff --git a/test/bridge/wrappers/MockSwap.t.sol b/test/bridge/wrappers/MockSwap.t.sol index 5688bfec2..2d82533c2 100644 --- a/test/bridge/wrappers/MockSwap.t.sol +++ b/test/bridge/wrappers/MockSwap.t.sol @@ -9,6 +9,8 @@ import "../../../contracts/bridge/wrappers/swap/MockSwap.sol"; import "../../../contracts/bridge/SynapseBridge.sol"; import "../../../contracts/bridge/SynapseERC20.sol"; +import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; + //solhint-disable func-name-mixedcase contract MockSwapTest is Test { MockSwap internal mockSwap; @@ -19,7 +21,8 @@ contract MockSwapTest is Test { function setUp() public { mockSwap = new MockSwap(); - bridge = new SynapseBridge(); + address implementation = address(new SynapseBridge()); + bridge = SynapseBridge(payable(Clones.clone(implementation))); bridge.initialize(); bridge.grantRole(bridge.NODEGROUP_ROLE(), address(this)); diff --git a/test/utils/Utilities06.sol b/test/utils/Utilities06.sol index 265a327d2..3c569832d 100644 --- a/test/utils/Utilities06.sol +++ b/test/utils/Utilities06.sol @@ -15,6 +15,7 @@ import {IWETH9} from "../../contracts/bridge/interfaces/IWETH9.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; contract ERC20Mock is ERC20, Ownable { constructor(string memory name_, uint8 decimals_) public ERC20(name_, name_) { @@ -185,7 +186,8 @@ contract Utilities06 is Test { } function deployBridge() public returns (SynapseBridge bridge) { - bridge = new SynapseBridge(); + address implementation = address(new SynapseBridge()); + bridge = SynapseBridge(payable(Clones.clone(implementation))); setupBridge(bridge); }