313 lines
12 KiB
Solidity
313 lines
12 KiB
Solidity
// SPDX-License-Identifier: MIT
|
|
pragma solidity ^0.8.20;
|
|
|
|
import "forge-std/Test.sol";
|
|
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
|
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
|
|
import "../../../contracts/bridge/atomic/AtomicBridgeCoordinator.sol";
|
|
import "../../../contracts/bridge/atomic/AtomicFeePolicy.sol";
|
|
import "../../../contracts/bridge/atomic/AtomicFulfillerRegistry.sol";
|
|
import "../../../contracts/bridge/atomic/AtomicLiquidityVault.sol";
|
|
import "../../../contracts/bridge/atomic/AtomicObligationEscrow.sol";
|
|
import "../../../contracts/bridge/atomic/AtomicSettlementRouter.sol";
|
|
import "../../../contracts/bridge/atomic/AtomicSlashingManager.sol";
|
|
import "../../../contracts/bridge/atomic/AtomicTypes.sol";
|
|
import "../../../contracts/bridge/atomic/AtomicX402SourceAdapter.sol";
|
|
import "../../../contracts/bridge/atomic/CWMultiTokenBridgeSettlementAdapter.sol";
|
|
import "../../dbis/MockMintableToken.sol";
|
|
|
|
contract MockBurnMintToken is ERC20 {
|
|
uint8 private immutable _decimals;
|
|
|
|
constructor(string memory name_, string memory symbol_, uint8 decimals_) ERC20(name_, symbol_) {
|
|
_decimals = decimals_;
|
|
}
|
|
|
|
function mint(address to, uint256 amount) external {
|
|
_mint(to, amount);
|
|
}
|
|
|
|
function burn(uint256 amount) external {
|
|
_burn(msg.sender, amount);
|
|
}
|
|
|
|
function decimals() public view override returns (uint8) {
|
|
return _decimals;
|
|
}
|
|
}
|
|
|
|
contract MockCWMultiTokenBridgeL1 {
|
|
uint256 public fee;
|
|
address public feeToken;
|
|
bytes32 public nextMessageId = keccak256("cw-message");
|
|
address public lastToken;
|
|
uint64 public lastSelector;
|
|
address public lastRecipient;
|
|
uint256 public lastAmount;
|
|
|
|
function setFee(uint256 fee_) external {
|
|
fee = fee_;
|
|
}
|
|
|
|
function setFeeToken(address feeToken_) external {
|
|
feeToken = feeToken_;
|
|
}
|
|
|
|
function calculateFee(address, uint64, address, uint256) external view returns (uint256) {
|
|
return fee;
|
|
}
|
|
|
|
function lockAndSend(address token, uint64 selector, address recipient, uint256 amount)
|
|
external
|
|
payable
|
|
returns (bytes32)
|
|
{
|
|
if (feeToken == address(0)) {
|
|
require(msg.value >= fee, "native fee");
|
|
} else {
|
|
IERC20(feeToken).transferFrom(msg.sender, address(this), fee);
|
|
}
|
|
IERC20(token).transferFrom(msg.sender, address(this), amount);
|
|
lastToken = token;
|
|
lastSelector = selector;
|
|
lastRecipient = recipient;
|
|
lastAmount = amount;
|
|
return nextMessageId;
|
|
}
|
|
}
|
|
|
|
contract AtomicX402IntentTest is Test {
|
|
uint64 internal constant SOURCE_CHAIN = 138;
|
|
uint64 internal constant POLYGON_SELECTOR = 4051577828743386545;
|
|
bytes32 internal constant SETTLEMENT_MODE = keccak256("CW_POLYGON_SETTLEMENT");
|
|
|
|
MockBurnMintToken internal legacyCUSDC;
|
|
MockBurnMintToken internal cUSDCV2;
|
|
MockMintableToken internal cWUSDCV2;
|
|
MockMintableToken internal bondToken;
|
|
|
|
AtomicLiquidityVault internal vault;
|
|
AtomicFulfillerRegistry internal registry;
|
|
AtomicFeePolicy internal feePolicy;
|
|
AtomicObligationEscrow internal escrow;
|
|
AtomicSettlementRouter internal router;
|
|
AtomicSlashingManager internal slashingManager;
|
|
AtomicBridgeCoordinator internal coordinator;
|
|
AtomicX402SourceAdapter internal sourceAdapter;
|
|
|
|
address internal user = address(0x1111);
|
|
address internal fulfiller = address(0x2222);
|
|
address internal protocolTreasury = address(0x3333);
|
|
bytes32 internal corridorId;
|
|
|
|
function setUp() public {
|
|
legacyCUSDC = new MockBurnMintToken("Legacy cUSDC", "cUSDC", 6);
|
|
cUSDCV2 = new MockBurnMintToken("V2 cUSDC", "cUSDC_V2", 6);
|
|
cWUSDCV2 = new MockMintableToken("Polygon cWUSDC V2", "cWUSDC_V2", 6, address(this));
|
|
bondToken = new MockMintableToken("Bond", "BOND", 6, address(this));
|
|
|
|
vault = new AtomicLiquidityVault(address(this));
|
|
registry = new AtomicFulfillerRegistry(address(bondToken), address(this));
|
|
feePolicy = new AtomicFeePolicy(address(this));
|
|
escrow = new AtomicObligationEscrow(address(this));
|
|
router = new AtomicSettlementRouter(address(this));
|
|
slashingManager = new AtomicSlashingManager(address(registry), address(this));
|
|
coordinator = new AtomicBridgeCoordinator(
|
|
address(vault),
|
|
address(registry),
|
|
address(escrow),
|
|
address(router),
|
|
address(feePolicy),
|
|
address(slashingManager),
|
|
protocolTreasury,
|
|
address(this)
|
|
);
|
|
|
|
vault.grantRole(vault.COORDINATOR_ROLE(), address(coordinator));
|
|
vault.grantRole(vault.RECONCILER_ROLE(), address(coordinator));
|
|
registry.grantRole(registry.COORDINATOR_ROLE(), address(coordinator));
|
|
registry.grantRole(registry.SLASHER_ROLE(), address(slashingManager));
|
|
escrow.grantRole(escrow.COORDINATOR_ROLE(), address(coordinator));
|
|
router.grantRole(router.COORDINATOR_ROLE(), address(coordinator));
|
|
slashingManager.grantRole(slashingManager.COORDINATOR_ROLE(), address(coordinator));
|
|
|
|
corridorId = coordinator.getCorridorId(SOURCE_CHAIN, POLYGON_SELECTOR, address(cUSDCV2), address(cWUSDCV2));
|
|
coordinator.configureCorridor(
|
|
AtomicTypes.CorridorConfig({
|
|
enabled: true,
|
|
degraded: false,
|
|
sourceChain: SOURCE_CHAIN,
|
|
destinationChain: POLYGON_SELECTOR,
|
|
assetIn: address(cUSDCV2),
|
|
assetOut: address(cWUSDCV2),
|
|
maxNotional: 500_000e6,
|
|
maxReservedBps: 8_000,
|
|
targetBuffer: 10_000e6,
|
|
maxSettlementBacklog: 250_000e6,
|
|
maxOracleDriftBps: 500,
|
|
fulfilmentTimeout: 1 days,
|
|
settlementTimeout: 2 days,
|
|
defaultSettlementMode: SETTLEMENT_MODE
|
|
})
|
|
);
|
|
feePolicy.setCorridorPolicy(corridorId, 100, 50, 12_000, 1_000, 1 days, 2 days);
|
|
vault.setTargetBuffer(corridorId, address(cWUSDCV2), 10_000e6);
|
|
|
|
registry.setFulfillerActive(fulfiller, true);
|
|
registry.setCorridorAuthorization(fulfiller, corridorId, true);
|
|
|
|
cWUSDCV2.mint(address(this), 100_000e6);
|
|
cWUSDCV2.approve(address(vault), type(uint256).max);
|
|
vault.fundCorridor(corridorId, address(cWUSDCV2), 100_000e6);
|
|
|
|
bondToken.mint(fulfiller, 100_000e6);
|
|
vm.startPrank(fulfiller);
|
|
bondToken.approve(address(registry), type(uint256).max);
|
|
registry.depositBond(50_000e6);
|
|
vm.stopPrank();
|
|
|
|
sourceAdapter = new AtomicX402SourceAdapter(
|
|
address(legacyCUSDC),
|
|
address(cUSDCV2),
|
|
address(cWUSDCV2),
|
|
address(coordinator),
|
|
SOURCE_CHAIN,
|
|
POLYGON_SELECTOR
|
|
);
|
|
coordinator.grantRole(coordinator.INTENT_ADAPTER_ROLE(), address(sourceAdapter));
|
|
}
|
|
|
|
function testLegacyCUSDCNormalizesToV2AndCreatesRefundableIntent() public {
|
|
legacyCUSDC.mint(user, 1_000e6);
|
|
|
|
vm.startPrank(user);
|
|
legacyCUSDC.approve(address(sourceAdapter), 1_000e6);
|
|
bytes32 obligationId = sourceAdapter.createIntent(
|
|
address(legacyCUSDC), 1_000e6, 990e6, user, block.timestamp + 1 hours, corridorId
|
|
);
|
|
vm.stopPrank();
|
|
|
|
assertEq(legacyCUSDC.balanceOf(user), 0);
|
|
assertEq(legacyCUSDC.totalSupply(), 0);
|
|
assertEq(cUSDCV2.balanceOf(address(escrow)), 1_000e6);
|
|
assertEq(cUSDCV2.balanceOf(address(sourceAdapter)), 0);
|
|
assertEq(coordinator.getRefundRecipient(obligationId), user);
|
|
|
|
AtomicTypes.AtomicIntent memory intent = coordinator.getIntent(obligationId);
|
|
assertEq(intent.assetIn, address(cUSDCV2));
|
|
assertEq(intent.assetOut, address(cWUSDCV2));
|
|
assertEq(intent.destinationChain, POLYGON_SELECTOR);
|
|
}
|
|
|
|
function testDirectCUSDCV2InputCreatesIntentWithoutLegacyBurn() public {
|
|
cUSDCV2.mint(user, 1_000e6);
|
|
legacyCUSDC.mint(user, 1_000e6);
|
|
|
|
vm.startPrank(user);
|
|
cUSDCV2.approve(address(sourceAdapter), 1_000e6);
|
|
sourceAdapter.createIntent(address(cUSDCV2), 1_000e6, 990e6, user, block.timestamp + 1 hours, corridorId);
|
|
vm.stopPrank();
|
|
|
|
assertEq(cUSDCV2.balanceOf(address(escrow)), 1_000e6);
|
|
assertEq(legacyCUSDC.balanceOf(user), 1_000e6);
|
|
}
|
|
|
|
function testExpiredAdapterIntentRefundsUserWithNormalizedV2() public {
|
|
legacyCUSDC.mint(user, 1_000e6);
|
|
|
|
vm.startPrank(user);
|
|
legacyCUSDC.approve(address(sourceAdapter), 1_000e6);
|
|
bytes32 obligationId = sourceAdapter.createIntent(
|
|
address(legacyCUSDC), 1_000e6, 990e6, user, block.timestamp + 30 minutes, corridorId
|
|
);
|
|
vm.stopPrank();
|
|
|
|
vm.warp(block.timestamp + 31 minutes);
|
|
coordinator.refundExpiredIntent(obligationId);
|
|
|
|
assertEq(cUSDCV2.balanceOf(user), 1_000e6);
|
|
assertEq(cUSDCV2.balanceOf(address(sourceAdapter)), 0);
|
|
}
|
|
|
|
function testAsyncDestinationIntentRecordsPolygonProofBeforeSettlement() public {
|
|
bytes32 asyncCorridorId = coordinator.getCorridorId(
|
|
SOURCE_CHAIN, POLYGON_SELECTOR, address(cUSDCV2), address(0x7476a64B37732698b9f729fdB373a7B65e9677e9)
|
|
);
|
|
coordinator.configureCorridor(
|
|
AtomicTypes.CorridorConfig({
|
|
enabled: true,
|
|
degraded: false,
|
|
sourceChain: SOURCE_CHAIN,
|
|
destinationChain: POLYGON_SELECTOR,
|
|
assetIn: address(cUSDCV2),
|
|
assetOut: address(0x7476a64B37732698b9f729fdB373a7B65e9677e9),
|
|
maxNotional: 500_000e6,
|
|
maxReservedBps: 8_000,
|
|
targetBuffer: 0,
|
|
maxSettlementBacklog: 250_000e6,
|
|
maxOracleDriftBps: 500,
|
|
fulfilmentTimeout: 1 days,
|
|
settlementTimeout: 2 days,
|
|
defaultSettlementMode: SETTLEMENT_MODE
|
|
})
|
|
);
|
|
coordinator.setAsyncDestinationCorridor(asyncCorridorId, true);
|
|
registry.setCorridorAuthorization(fulfiller, asyncCorridorId, true);
|
|
|
|
AtomicX402SourceAdapter asyncSourceAdapter = new AtomicX402SourceAdapter(
|
|
address(legacyCUSDC),
|
|
address(cUSDCV2),
|
|
address(0x7476a64B37732698b9f729fdB373a7B65e9677e9),
|
|
address(coordinator),
|
|
SOURCE_CHAIN,
|
|
POLYGON_SELECTOR
|
|
);
|
|
coordinator.grantRole(coordinator.INTENT_ADAPTER_ROLE(), address(asyncSourceAdapter));
|
|
|
|
legacyCUSDC.mint(user, 1_100e6);
|
|
vm.startPrank(user);
|
|
legacyCUSDC.approve(address(asyncSourceAdapter), 1_100e6);
|
|
bytes32 obligationId = asyncSourceAdapter.createIntent(
|
|
address(legacyCUSDC), 1_100e6, 1_000e6, user, block.timestamp + 1 hours, asyncCorridorId
|
|
);
|
|
vm.stopPrank();
|
|
|
|
AtomicTypes.CorridorLiquidityState memory state =
|
|
vault.getCorridorLiquidityState(asyncCorridorId, address(0x7476a64B37732698b9f729fdB373a7B65e9677e9));
|
|
assertEq(state.reservedLiquidity, 0);
|
|
|
|
vm.prank(fulfiller);
|
|
coordinator.submitAsyncDestinationFulfillment(
|
|
obligationId, SETTLEMENT_MODE, keccak256("polygon-tx"), keccak256("invoice")
|
|
);
|
|
|
|
AtomicTypes.AtomicObligation memory obligation = coordinator.getObligation(obligationId);
|
|
assertEq(uint8(obligation.status), uint8(AtomicTypes.ObligationStatus.Fulfilled));
|
|
assertEq(obligation.fulfiller, fulfiller);
|
|
}
|
|
|
|
function testSettlementAdapterCallsCwBridgeLockAndSend() public {
|
|
MockCWMultiTokenBridgeL1 bridge = new MockCWMultiTokenBridgeL1();
|
|
bridge.setFee(0.01 ether);
|
|
CWMultiTokenBridgeSettlementAdapter adapter =
|
|
new CWMultiTokenBridgeSettlementAdapter(address(bridge), POLYGON_SELECTOR);
|
|
|
|
cUSDCV2.mint(address(adapter), 1_000e6);
|
|
bytes32 settlementId = adapter.executeSettlement{value: 0.01 ether}(
|
|
keccak256("obligation"),
|
|
address(cUSDCV2),
|
|
1_000e6,
|
|
fulfiller,
|
|
abi.encode(fulfiller, POLYGON_SELECTOR, uint256(0.01 ether))
|
|
);
|
|
|
|
assertEq(settlementId, bridge.nextMessageId());
|
|
assertEq(bridge.lastToken(), address(cUSDCV2));
|
|
assertEq(bridge.lastSelector(), POLYGON_SELECTOR);
|
|
assertEq(bridge.lastRecipient(), fulfiller);
|
|
assertEq(bridge.lastAmount(), 1_000e6);
|
|
assertEq(cUSDCV2.balanceOf(address(bridge)), 1_000e6);
|
|
}
|
|
}
|