- CCIP/trustless bridge contracts, GRU tokens, DEX/PMM tests, reserve vault. - Token-aggregation service routes, planner, chain config, relay env templates. - Config snapshots and multi-chain deployment markdown updates. - gitignore services/btc-intake/dist/ (tsc output); do not track dist. Run forge build && forge test before deploy (large solc graph). Made-with: Cursor
136 lines
5.1 KiB
Solidity
136 lines
5.1 KiB
Solidity
// SPDX-License-Identifier: MIT
|
|
pragma solidity ^0.8.20;
|
|
|
|
import {Test} from "forge-std/Test.sol";
|
|
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
|
|
|
import {IRouterClient} from "../../contracts/ccip/IRouterClient.sol";
|
|
import {CWMultiTokenBridgeL1} from "../../contracts/bridge/CWMultiTokenBridgeL1.sol";
|
|
import {CWMultiTokenBridgeL2} from "../../contracts/bridge/CWMultiTokenBridgeL2.sol";
|
|
import {CompliantWrappedToken} from "../../contracts/tokens/CompliantWrappedToken.sol";
|
|
|
|
contract MockCanonicalBTC is ERC20 {
|
|
constructor() ERC20("Bitcoin (Compliant)", "cBTC") {}
|
|
|
|
function decimals() public pure override returns (uint8) {
|
|
return 8;
|
|
}
|
|
|
|
function mint(address to, uint256 amount) external {
|
|
_mint(to, amount);
|
|
}
|
|
}
|
|
|
|
contract MockRouterBTC is IRouterClient {
|
|
bytes32 public nextMessageId = keccak256("btc-message");
|
|
EVM2AnyMessage internal _lastMessage;
|
|
|
|
function ccipSend(
|
|
uint64 destinationChainSelector,
|
|
EVM2AnyMessage memory message
|
|
) external payable returns (bytes32 messageId, uint256 fees) {
|
|
destinationChainSelector;
|
|
_lastMessage = message;
|
|
return (nextMessageId, fees);
|
|
}
|
|
|
|
function getFee(uint64, EVM2AnyMessage memory) external pure returns (uint256) {
|
|
return 0;
|
|
}
|
|
|
|
function getSupportedTokens(uint64) external pure returns (address[] memory tokens) {
|
|
tokens = new address[](0);
|
|
}
|
|
|
|
function lastMessage() external view returns (bytes memory receiver, bytes memory data) {
|
|
return (_lastMessage.receiver, _lastMessage.data);
|
|
}
|
|
}
|
|
|
|
contract CWMultiTokenBridgeBTCTest is Test {
|
|
uint64 internal constant CHAIN138_SELECTOR = 138;
|
|
uint64 internal constant ETHEREUM_SELECTOR = 5009297550715157269;
|
|
|
|
address internal user = address(0xBEEF);
|
|
|
|
MockRouterBTC internal router138;
|
|
MockRouterBTC internal routerEth;
|
|
MockCanonicalBTC internal canonical;
|
|
CompliantWrappedToken internal wrapped;
|
|
CWMultiTokenBridgeL1 internal l1Bridge;
|
|
CWMultiTokenBridgeL2 internal l2Bridge;
|
|
|
|
function setUp() public {
|
|
router138 = new MockRouterBTC();
|
|
routerEth = new MockRouterBTC();
|
|
canonical = new MockCanonicalBTC();
|
|
wrapped = new CompliantWrappedToken("Wrapped cBTC", "cWBTC", 8, address(this));
|
|
|
|
l1Bridge = new CWMultiTokenBridgeL1(address(router138), address(0x138138), address(0));
|
|
l2Bridge = new CWMultiTokenBridgeL2(address(routerEth), address(0x010101), address(0));
|
|
|
|
l1Bridge.configureSupportedCanonicalToken(address(canonical), true);
|
|
l1Bridge.configureDestination(address(canonical), ETHEREUM_SELECTOR, address(l2Bridge), true);
|
|
l2Bridge.configureDestination(CHAIN138_SELECTOR, address(l1Bridge), true);
|
|
l2Bridge.configureTokenPair(address(canonical), address(wrapped));
|
|
|
|
wrapped.grantRole(wrapped.MINTER_ROLE(), address(l2Bridge));
|
|
wrapped.grantRole(wrapped.BURNER_ROLE(), address(l2Bridge));
|
|
|
|
canonical.mint(user, 2_100_000_000_000_000);
|
|
}
|
|
|
|
function testRoundTripPreservesSatoshiPrecision() public {
|
|
uint256 amount = 125_000_000;
|
|
|
|
vm.startPrank(user);
|
|
canonical.approve(address(l1Bridge), amount);
|
|
bytes32 outboundMessageId = l1Bridge.lockAndSend(address(canonical), ETHEREUM_SELECTOR, user, amount);
|
|
vm.stopPrank();
|
|
|
|
(, bytes memory outboundData) = router138.lastMessage();
|
|
vm.prank(address(0x010101));
|
|
l2Bridge.ccipReceive(_message(outboundMessageId, CHAIN138_SELECTOR, address(l1Bridge), outboundData));
|
|
|
|
assertEq(wrapped.balanceOf(user), amount);
|
|
assertEq(wrapped.totalSupply(), amount);
|
|
|
|
vm.prank(user);
|
|
bytes32 returnMessageId = l2Bridge.burnAndSend(address(wrapped), CHAIN138_SELECTOR, user, amount);
|
|
|
|
(, bytes memory returnData) = routerEth.lastMessage();
|
|
vm.prank(address(0x138138));
|
|
l1Bridge.ccipReceive(_message(returnMessageId, ETHEREUM_SELECTOR, address(l2Bridge), returnData));
|
|
|
|
assertEq(canonical.balanceOf(user), 2_100_000_000_000_000);
|
|
assertEq(wrapped.totalSupply(), 0);
|
|
assertEq(l1Bridge.totalOutstanding(address(canonical)), 0);
|
|
}
|
|
|
|
function testPerDestinationOutstandingCapStillAppliesForBTCRoute() public {
|
|
l1Bridge.setMaxOutstanding(address(canonical), ETHEREUM_SELECTOR, 100_000_000);
|
|
|
|
vm.startPrank(user);
|
|
canonical.approve(address(l1Bridge), 125_000_000);
|
|
vm.expectRevert(bytes("CWMultiTokenBridgeL1: exceeds escrow capacity"));
|
|
l1Bridge.lockAndSend(address(canonical), ETHEREUM_SELECTOR, user, 125_000_000);
|
|
vm.stopPrank();
|
|
}
|
|
|
|
function _message(
|
|
bytes32 messageId,
|
|
uint64 sourceChainSelector,
|
|
address sender,
|
|
bytes memory data
|
|
) internal pure returns (IRouterClient.Any2EVMMessage memory message) {
|
|
IRouterClient.TokenAmount[] memory noTokens = new IRouterClient.TokenAmount[](0);
|
|
message = IRouterClient.Any2EVMMessage({
|
|
messageId: messageId,
|
|
sourceChainSelector: sourceChainSelector,
|
|
sender: abi.encode(sender),
|
|
data: data,
|
|
tokenAmounts: noTokens
|
|
});
|
|
}
|
|
}
|