196 lines
7.4 KiB
Solidity
196 lines
7.4 KiB
Solidity
// SPDX-License-Identifier: MIT
|
|
pragma solidity ^0.8.19;
|
|
|
|
import {Test} from "forge-std/Test.sol";
|
|
import "../../../contracts/bridge/trustless/DualRouterBridgeSwapCoordinator.sol";
|
|
import "../../../contracts/bridge/trustless/ChallengeManager.sol";
|
|
import "../../../contracts/bridge/trustless/BondManager.sol";
|
|
import "../../../contracts/bridge/trustless/LiquidityPoolETH.sol";
|
|
import "../../../contracts/bridge/trustless/InboxETH.sol";
|
|
import "../../../contracts/bridge/trustless/EnhancedSwapRouter.sol";
|
|
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
|
|
|
contract MockERC20 is ERC20 {
|
|
constructor(string memory name, string memory symbol) ERC20(name, symbol) {
|
|
_mint(msg.sender, 1000000 ether);
|
|
}
|
|
function mint(address to, uint256 amount) external {
|
|
_mint(to, amount);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Mock SwapRouter: implements swapToStablecoin(..., bytes) returns (uint256).
|
|
* Transfers stablecoinToken to msg.sender (caller) for amountIn (1:1 mock).
|
|
*/
|
|
contract MockBasicSwapRouter {
|
|
function swapToStablecoin(
|
|
LiquidityPoolETH.AssetType,
|
|
address stablecoinToken,
|
|
uint256 amountIn,
|
|
uint256,
|
|
bytes calldata
|
|
) external payable returns (uint256) {
|
|
if (msg.value > 0) {
|
|
// Caller sent ETH; in real router it would wrap and swap. We just transfer stablecoin.
|
|
}
|
|
uint256 out = amountIn; // 1:1 mock
|
|
require(IERC20(stablecoinToken).transfer(msg.sender, out), "transfer failed");
|
|
return out;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Mock EnhancedSwapRouter: implements swapToStablecoin(..., SwapProvider) returns (uint256, SwapProvider).
|
|
*/
|
|
contract MockEnhancedSwapRouter {
|
|
function swapToStablecoin(
|
|
LiquidityPoolETH.AssetType,
|
|
address stablecoinToken,
|
|
uint256 amountIn,
|
|
uint256,
|
|
EnhancedSwapRouter.SwapProvider
|
|
) external payable returns (uint256 amountOut, EnhancedSwapRouter.SwapProvider providerUsed) {
|
|
uint256 out = amountIn;
|
|
require(IERC20(stablecoinToken).transfer(msg.sender, out), "transfer failed");
|
|
return (out, EnhancedSwapRouter.SwapProvider.UniswapV3);
|
|
}
|
|
}
|
|
|
|
contract DualRouterBridgeSwapCoordinatorTest is Test {
|
|
DualRouterBridgeSwapCoordinator public coordinator;
|
|
BondManager public bondManager;
|
|
ChallengeManager public challengeManager;
|
|
LiquidityPoolETH public liquidityPool;
|
|
InboxETH public inbox;
|
|
MockBasicSwapRouter public basicRouter;
|
|
MockEnhancedSwapRouter public enhancedRouter;
|
|
|
|
MockERC20 public weth;
|
|
MockERC20 public usdt;
|
|
|
|
address public deployer = address(0xDE01);
|
|
address public relayer = address(0x2222);
|
|
address public lpProvider = address(0x3333);
|
|
address public recipient = address(0x4444);
|
|
|
|
uint256 constant BOND_MULTIPLIER = 11000;
|
|
uint256 constant MIN_BOND = 1 ether;
|
|
uint256 constant CHALLENGE_WINDOW = 30 minutes;
|
|
uint256 constant LP_FEE_BPS = 5;
|
|
uint256 constant MIN_LIQUIDITY_RATIO_BPS = 11000;
|
|
|
|
function setUp() public {
|
|
vm.startPrank(deployer);
|
|
|
|
weth = new MockERC20("WETH", "WETH");
|
|
usdt = new MockERC20("USDT", "USDT");
|
|
|
|
bondManager = new BondManager(BOND_MULTIPLIER, MIN_BOND);
|
|
challengeManager = new ChallengeManager(address(bondManager), CHALLENGE_WINDOW);
|
|
liquidityPool = new LiquidityPoolETH(address(weth), LP_FEE_BPS, MIN_LIQUIDITY_RATIO_BPS);
|
|
inbox = new InboxETH(address(bondManager), address(challengeManager), address(liquidityPool));
|
|
|
|
basicRouter = new MockBasicSwapRouter();
|
|
enhancedRouter = new MockEnhancedSwapRouter();
|
|
|
|
coordinator = new DualRouterBridgeSwapCoordinator(
|
|
address(inbox),
|
|
address(liquidityPool),
|
|
address(basicRouter),
|
|
address(enhancedRouter),
|
|
address(challengeManager)
|
|
);
|
|
|
|
liquidityPool.authorizeRelease(address(coordinator));
|
|
|
|
usdt.mint(address(basicRouter), 1000 ether);
|
|
usdt.mint(address(enhancedRouter), 1000 ether);
|
|
|
|
vm.deal(lpProvider, 1000 ether);
|
|
vm.deal(relayer, 100 ether);
|
|
|
|
vm.stopPrank();
|
|
}
|
|
|
|
function _setupFinalizedClaim(uint256 depositAmount) internal returns (uint256 depositId) {
|
|
depositId = uint256(keccak256(abi.encodePacked(block.timestamp, recipient, depositAmount)));
|
|
vm.prank(address(inbox));
|
|
liquidityPool.addPendingClaim(depositAmount, LiquidityPoolETH.AssetType.ETH);
|
|
|
|
// Simulate ChallengeManager having a finalized claim (we don't have Inbox.submitClaim in this test,
|
|
// so we need to push claim state into ChallengeManager - but ChallengeManager is deployed and
|
|
// we can't easily inject a claim without going through Inbox. So we use a different approach:
|
|
// use the real flow: deploy Lockbox138, user deposits, relayer submits claim, time passes, finalize.
|
|
// For unit test we can use vm.mockCall to make getClaim return a finalized claim.
|
|
vm.mockCall(
|
|
address(challengeManager),
|
|
abi.encodeWithSelector(ChallengeManager.getClaim.selector, depositId),
|
|
abi.encode(
|
|
ChallengeManager.Claim({
|
|
depositId: depositId,
|
|
asset: address(0),
|
|
recipient: recipient,
|
|
amount: depositAmount,
|
|
challengeWindowEnd: block.timestamp,
|
|
finalized: true,
|
|
challenged: false
|
|
})
|
|
)
|
|
);
|
|
return depositId;
|
|
}
|
|
|
|
function testDeployment() public view {
|
|
assertEq(address(coordinator.inbox()), address(inbox));
|
|
assertEq(address(coordinator.liquidityPool()), address(liquidityPool));
|
|
assertEq(address(coordinator.swapRouter()), address(basicRouter));
|
|
assertEq(address(coordinator.enhancedSwapRouter()), address(enhancedRouter));
|
|
assertEq(address(coordinator.challengeManager()), address(challengeManager));
|
|
}
|
|
|
|
function testCanSwapWhenFinalized() public {
|
|
uint256 depositId = 1;
|
|
vm.mockCall(
|
|
address(challengeManager),
|
|
abi.encodeWithSelector(ChallengeManager.getClaim.selector, depositId),
|
|
abi.encode(
|
|
ChallengeManager.Claim({
|
|
depositId: depositId,
|
|
asset: address(0),
|
|
recipient: recipient,
|
|
amount: 1 ether,
|
|
challengeWindowEnd: block.timestamp,
|
|
finalized: true,
|
|
challenged: false
|
|
})
|
|
)
|
|
);
|
|
(bool canSwap, string memory reason) = coordinator.canSwap(depositId);
|
|
assertTrue(canSwap);
|
|
assertEq(reason, "");
|
|
}
|
|
|
|
function testCanSwapWhenNotFinalized() public {
|
|
uint256 depositId = 2;
|
|
vm.mockCall(
|
|
address(challengeManager),
|
|
abi.encodeWithSelector(ChallengeManager.getClaim.selector, depositId),
|
|
abi.encode(
|
|
ChallengeManager.Claim({
|
|
depositId: depositId,
|
|
asset: address(0),
|
|
recipient: recipient,
|
|
amount: 1 ether,
|
|
challengeWindowEnd: block.timestamp,
|
|
finalized: false,
|
|
challenged: false
|
|
})
|
|
)
|
|
);
|
|
(bool canSwap, string memory reason) = coordinator.canSwap(depositId);
|
|
assertFalse(canSwap);
|
|
assertEq(reason, "Claim not finalized");
|
|
}
|
|
}
|