feat(atomic): add x402 intent bridge adapters
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
84
test/bridge/atomic/AtomicCwUsdcUsdcPurchaseV2.t.sol
Normal file
84
test/bridge/atomic/AtomicCwUsdcUsdcPurchaseV2.t.sol
Normal file
@@ -0,0 +1,84 @@
|
||||
// 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 {AtomicCwUsdcUsdcPurchaseV2} from "../../../contracts/x402/AtomicCwUsdcUsdcPurchaseV2.sol";
|
||||
import "../../dbis/MockMintableToken.sol";
|
||||
|
||||
contract MockERC3009Token is ERC20 {
|
||||
mapping(address => mapping(bytes32 => bool)) public authorizationState;
|
||||
|
||||
constructor() ERC20("USD Coin (Compliant Wrapped V2)", "cWUSDC_V2") {}
|
||||
|
||||
function mint(address to, uint256 amount) external {
|
||||
_mint(to, amount);
|
||||
}
|
||||
|
||||
function transferWithAuthorization(
|
||||
address from,
|
||||
address to,
|
||||
uint256 value,
|
||||
uint256 validAfter,
|
||||
uint256 validBefore,
|
||||
bytes32 nonce,
|
||||
uint8,
|
||||
bytes32,
|
||||
bytes32
|
||||
) external {
|
||||
require(block.timestamp > validAfter, "not yet valid");
|
||||
require(block.timestamp < validBefore, "expired");
|
||||
require(!authorizationState[from][nonce], "used");
|
||||
authorizationState[from][nonce] = true;
|
||||
_transfer(from, to, value);
|
||||
}
|
||||
|
||||
function decimals() public pure override returns (uint8) {
|
||||
return 6;
|
||||
}
|
||||
}
|
||||
|
||||
contract AtomicCwUsdcUsdcPurchaseV2Test is Test {
|
||||
address internal payer;
|
||||
address internal treasury = address(0xCAFE);
|
||||
address internal invoiceRecipient = address(0xBEEF);
|
||||
address internal owner = address(0x1234);
|
||||
|
||||
MockERC3009Token internal cWUSDCV2;
|
||||
MockMintableToken internal usdc;
|
||||
AtomicCwUsdcUsdcPurchaseV2 internal purchase;
|
||||
|
||||
function setUp() public {
|
||||
payer = address(0xA11CE);
|
||||
cWUSDCV2 = new MockERC3009Token();
|
||||
cWUSDCV2.mint(payer, 1_000e6);
|
||||
usdc = new MockMintableToken("Polygon USDC", "USDC", 6, address(this));
|
||||
purchase = new AtomicCwUsdcUsdcPurchaseV2(address(cWUSDCV2), address(usdc), treasury, owner);
|
||||
usdc.mint(address(purchase), 1_000e6);
|
||||
}
|
||||
|
||||
function testInvoicePurchaseTransfersCwToTreasuryAndUsdcToRecipient() public {
|
||||
uint256 amount = 110_000;
|
||||
uint256 validAfter = block.timestamp - 1;
|
||||
uint256 validBefore = block.timestamp + 1 days;
|
||||
bytes32 nonce = keccak256("invoice-auth");
|
||||
|
||||
purchase.atomicX402InvoicePurchase(
|
||||
keccak256("obligation"),
|
||||
keccak256("invoice"),
|
||||
payer,
|
||||
invoiceRecipient,
|
||||
amount,
|
||||
validAfter,
|
||||
validBefore,
|
||||
nonce,
|
||||
0,
|
||||
bytes32(0),
|
||||
bytes32(0)
|
||||
);
|
||||
|
||||
assertEq(cWUSDCV2.balanceOf(treasury), amount);
|
||||
assertEq(usdc.balanceOf(invoiceRecipient), amount);
|
||||
assertTrue(cWUSDCV2.authorizationState(payer, nonce));
|
||||
}
|
||||
}
|
||||
255
test/bridge/atomic/AtomicX402Intent.t.sol
Normal file
255
test/bridge/atomic/AtomicX402Intent.t.sol
Normal file
@@ -0,0 +1,255 @@
|
||||
// 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 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user