feat(atomic): add x402 intent bridge adapters

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
defiQUG
2026-07-08 10:02:12 -07:00
parent 869d61bcc7
commit 9402317eb2
7 changed files with 848 additions and 46 deletions

View File

@@ -15,6 +15,7 @@ import {AtomicSlashingManager} from "./AtomicSlashingManager.sol";
contract AtomicBridgeCoordinator is AccessControl, ReentrancyGuard, IAtomicBridgeCoordinator {
bytes32 public constant CORRIDOR_MANAGER_ROLE = keccak256("CORRIDOR_MANAGER_ROLE");
bytes32 public constant SETTLEMENT_MANAGER_ROLE = keccak256("SETTLEMENT_MANAGER_ROLE");
bytes32 public constant INTENT_ADAPTER_ROLE = keccak256("INTENT_ADAPTER_ROLE");
IAtomicLiquidityVault public immutable liquidityVault;
IAtomicFulfillerRegistry public immutable fulfillerRegistry;
@@ -41,11 +42,16 @@ contract AtomicBridgeCoordinator is AccessControl, ReentrancyGuard, IAtomicBridg
mapping(bytes32 => AtomicTypes.AtomicIntent) private _intents;
mapping(bytes32 => AtomicTypes.AtomicCommitment) private _commitments;
mapping(bytes32 => AtomicTypes.AtomicObligation) private _obligations;
mapping(bytes32 => address) private _refundRecipients;
event CorridorConfigured(bytes32 indexed corridorId, address indexed assetIn, address indexed assetOut);
event IntentCreated(bytes32 indexed obligationId, bytes32 indexed corridorId, bytes32 indexed intentId, address sender);
event IntentCreated(
bytes32 indexed obligationId, bytes32 indexed corridorId, bytes32 indexed intentId, address sender
);
event CommitmentAccepted(bytes32 indexed obligationId, address indexed fulfiller, uint256 bondAmount);
event SettlementInitiated(bytes32 indexed obligationId, bytes32 indexed settlementId, bytes32 indexed settlementMode);
event SettlementInitiated(
bytes32 indexed obligationId, bytes32 indexed settlementId, bytes32 indexed settlementMode
);
event SettlementConfirmed(bytes32 indexed obligationId, uint256 replenishAmount);
event IntentRefunded(bytes32 indexed obligationId, uint256 refundedAmount);
event CorridorDegraded(bytes32 indexed corridorId, bytes32 indexed obligationId, bytes32 reason);
@@ -61,6 +67,8 @@ contract AtomicBridgeCoordinator is AccessControl, ReentrancyGuard, IAtomicBridg
error DeadlineNotReached();
error SettlementTimeoutNotReached();
error MinimumReplenishNotMet();
error ZeroPayer();
error ZeroRefundRecipient();
constructor(
address liquidityVault_,
@@ -84,12 +92,11 @@ contract AtomicBridgeCoordinator is AccessControl, ReentrancyGuard, IAtomicBridg
_grantRole(SETTLEMENT_MANAGER_ROLE, admin);
}
function getCorridorId(
uint64 sourceChain,
uint64 destinationChain,
address assetIn,
address assetOut
) public pure returns (bytes32) {
function getCorridorId(uint64 sourceChain, uint64 destinationChain, address assetIn, address assetOut)
public
pure
returns (bytes32)
{
return keccak256(abi.encode(sourceChain, destinationChain, assetIn, assetOut));
}
@@ -104,15 +111,36 @@ contract AtomicBridgeCoordinator is AccessControl, ReentrancyGuard, IAtomicBridg
}
function createIntent(CreateIntentParams calldata p) external nonReentrant returns (bytes32 obligationId) {
obligationId = _createIntent(p, msg.sender, msg.sender);
}
function createIntentFor(CreateIntentParams calldata p, address payer, address refundTo)
external
nonReentrant
onlyRole(INTENT_ADAPTER_ROLE)
returns (bytes32 obligationId)
{
if (payer == address(0)) revert ZeroPayer();
if (refundTo == address(0)) revert ZeroRefundRecipient();
obligationId = _createIntent(p, payer, refundTo);
}
function _createIntent(CreateIntentParams calldata p, address payer, address refundTo)
internal
returns (bytes32 obligationId)
{
bytes32 corridorId = getCorridorId(p.sourceChain, p.destinationChain, p.assetIn, p.assetOut);
AtomicTypes.CorridorConfig memory cfg = _corridors[corridorId];
if (!cfg.enabled) revert CorridorDisabled();
if (cfg.degraded) revert CorridorDegradedError();
if (cfg.assetIn != p.assetIn || cfg.assetOut != p.assetOut) revert InvalidCorridor();
if (p.deadline <= block.timestamp || p.deadline > block.timestamp + cfg.fulfilmentTimeout) revert InvalidDeadline();
if (p.deadline <= block.timestamp || p.deadline > block.timestamp + cfg.fulfilmentTimeout) {
revert InvalidDeadline();
}
if (p.amountIn > cfg.maxNotional) revert MaxNotionalExceeded();
AtomicTypes.CorridorLiquidityState memory state = liquidityVault.getCorridorLiquidityState(corridorId, p.assetOut);
AtomicTypes.CorridorLiquidityState memory state =
liquidityVault.getCorridorLiquidityState(corridorId, p.assetOut);
if (p.minAmountOut > state.freeLiquidity) revert ReservedLiquidityLimitExceeded();
if (state.totalLiquidity > 0) {
uint256 nextReserved = state.reservedLiquidity + p.minAmountOut;
@@ -124,17 +152,10 @@ contract AtomicBridgeCoordinator is AccessControl, ReentrancyGuard, IAtomicBridg
bytes32 intentId = keccak256(
abi.encode(
block.chainid,
msg.sender,
++intentNonce,
corridorId,
p.amountIn,
p.minAmountOut,
p.deadline,
p.routeId
block.chainid, payer, ++intentNonce, corridorId, p.amountIn, p.minAmountOut, p.deadline, p.routeId
)
);
obligationId = keccak256(abi.encode(intentId, p.recipient));
obligationId = keccak256(abi.encode(intentId, p.recipient, refundTo));
_intents[obligationId] = AtomicTypes.AtomicIntent({
sourceChain: p.sourceChain,
@@ -158,10 +179,11 @@ contract AtomicBridgeCoordinator is AccessControl, ReentrancyGuard, IAtomicBridg
settlementInitiatedAt: 0
});
obligationEscrow.escrowFunds(obligationId, p.assetIn, msg.sender, p.amountIn);
_refundRecipients[obligationId] = refundTo;
obligationEscrow.escrowFunds(obligationId, p.assetIn, payer, p.amountIn);
liquidityVault.reserveLiquidity(corridorId, p.assetOut, obligationId, p.minAmountOut);
emit IntentCreated(obligationId, corridorId, intentId, msg.sender);
emit IntentCreated(obligationId, corridorId, intentId, payer);
}
function submitCommitment(bytes32 obligationId, bytes32 settlementMode) external nonReentrant {
@@ -203,19 +225,10 @@ contract AtomicBridgeCoordinator is AccessControl, ReentrancyGuard, IAtomicBridg
if (obligation.status != AtomicTypes.ObligationStatus.Fulfilled) revert InvalidStatus();
bytes32 corridorId = getCorridorId(intent.sourceChain, intent.destinationChain, intent.assetIn, intent.assetOut);
uint256 settlementAmount = _releaseEscrowForSettlement(
obligationId,
corridorId,
intent.amountIn,
commitment.fulfiller
);
uint256 settlementAmount =
_releaseEscrowForSettlement(obligationId, corridorId, intent.amountIn, commitment.fulfiller);
settlementId = _executeSettlement(
obligationId,
commitment.settlementMode,
intent.assetIn,
settlementAmount,
intent.recipient,
settlementData
obligationId, commitment.settlementMode, intent.assetIn, settlementAmount, intent.recipient, settlementData
);
obligation.status = AtomicTypes.ObligationStatus.SettlementPending;
@@ -223,12 +236,10 @@ contract AtomicBridgeCoordinator is AccessControl, ReentrancyGuard, IAtomicBridg
emit SettlementInitiated(obligationId, settlementId, commitment.settlementMode);
}
function _releaseEscrowForSettlement(
bytes32 obligationId,
bytes32 corridorId,
uint256 amountIn,
address fulfiller
) internal returns (uint256 settlementAmount) {
function _releaseEscrowForSettlement(bytes32 obligationId, bytes32 corridorId, uint256 amountIn, address fulfiller)
internal
returns (uint256 settlementAmount)
{
(uint256 fulfillerFee, uint256 protocolFee) = feePolicy.quoteFees(corridorId, amountIn);
settlementAmount = amountIn - fulfillerFee - protocolFee;
@@ -250,12 +261,7 @@ contract AtomicBridgeCoordinator is AccessControl, ReentrancyGuard, IAtomicBridg
bytes calldata settlementData
) internal returns (bytes32 settlementId) {
settlementId = settlementRouter.executeSettlement{value: msg.value}(
obligationId,
settlementMode,
assetIn,
settlementAmount,
recipient,
settlementData
obligationId, settlementMode, assetIn, settlementAmount, recipient, settlementData
);
}
@@ -284,8 +290,12 @@ contract AtomicBridgeCoordinator is AccessControl, ReentrancyGuard, IAtomicBridg
bytes32 corridorId = getCorridorId(intent.sourceChain, intent.destinationChain, intent.assetIn, intent.assetOut);
(, address payer,,,) = obligationEscrow.escrows(obligationId);
address refundTo = _refundRecipients[obligationId];
if (refundTo == address(0)) {
refundTo = payer;
}
liquidityVault.releaseReservation(obligationId);
uint256 refunded = obligationEscrow.refundRemaining(obligationId, payer);
uint256 refunded = obligationEscrow.refundRemaining(obligationId, refundTo);
_corridors[corridorId].degraded = true;
obligation.status = AtomicTypes.ObligationStatus.Refunded;
emit IntentRefunded(obligationId, refunded);
@@ -321,4 +331,8 @@ contract AtomicBridgeCoordinator is AccessControl, ReentrancyGuard, IAtomicBridg
function getObligation(bytes32 obligationId) external view returns (AtomicTypes.AtomicObligation memory) {
return _obligations[obligationId];
}
function getRefundRecipient(bytes32 obligationId) external view returns (address) {
return _refundRecipients[obligationId];
}
}

View File

@@ -0,0 +1,119 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {AtomicBridgeCoordinator} from "./AtomicBridgeCoordinator.sol";
interface ILegacyBurnableERC20 is IERC20 {
function burn(uint256 amount) external;
}
interface IMintableERC20 is IERC20 {
function mint(address to, uint256 amount) external;
}
interface IAtomicObligationEscrowAddress {
function obligationEscrow() external view returns (address);
}
/**
* @title AtomicX402SourceAdapter
* @notice Normalizes Chain 138 cUSDC/cUSDC_V2 input into cUSDC_V2 before creating
* an atomic x402 obligation. Legacy cUSDC is burned by this adapter after
* transfer-in; cUSDC_V2 is escrowed as the coordinator assetIn.
*/
contract AtomicX402SourceAdapter {
using SafeERC20 for IERC20;
uint64 public immutable sourceChain;
uint64 public immutable destinationChainSelector;
ILegacyBurnableERC20 public immutable legacyCUSDC;
IMintableERC20 public immutable cUSDCV2;
IERC20 public immutable cWUSDCV2;
AtomicBridgeCoordinator public immutable coordinator;
address public immutable escrow;
event SourceIntentCreated(
bytes32 indexed obligationId,
address indexed payer,
address indexed sourceToken,
address recipient,
uint256 amountIn,
uint256 minAmountOut
);
error UnsupportedSourceToken(address token);
error ZeroAddress();
error ZeroAmount();
constructor(
address legacyCUSDC_,
address cUSDCV2_,
address cWUSDCV2_,
address coordinator_,
uint64 sourceChain_,
uint64 destinationChainSelector_
) {
if (
legacyCUSDC_ == address(0) || cUSDCV2_ == address(0) || cWUSDCV2_ == address(0)
|| coordinator_ == address(0)
) {
revert ZeroAddress();
}
legacyCUSDC = ILegacyBurnableERC20(legacyCUSDC_);
cUSDCV2 = IMintableERC20(cUSDCV2_);
cWUSDCV2 = IERC20(cWUSDCV2_);
coordinator = AtomicBridgeCoordinator(coordinator_);
sourceChain = sourceChain_;
destinationChainSelector = destinationChainSelector_;
escrow = IAtomicObligationEscrowAddress(coordinator_).obligationEscrow();
IERC20(cUSDCV2_).forceApprove(escrow, type(uint256).max);
}
function createIntent(
address sourceToken,
uint256 amountIn,
uint256 minAmountOut,
address recipient,
uint256 deadline,
bytes32 routeId
) external returns (bytes32 obligationId) {
if (amountIn == 0 || minAmountOut == 0) revert ZeroAmount();
if (recipient == address(0)) revert ZeroAddress();
_normalizeToV2(sourceToken, amountIn);
obligationId = coordinator.createIntentFor(
AtomicBridgeCoordinator.CreateIntentParams({
sourceChain: sourceChain,
destinationChain: destinationChainSelector,
assetIn: address(cUSDCV2),
assetOut: address(cWUSDCV2),
amountIn: amountIn,
minAmountOut: minAmountOut,
recipient: recipient,
deadline: deadline,
routeId: routeId
}),
address(this),
msg.sender
);
emit SourceIntentCreated(obligationId, msg.sender, sourceToken, recipient, amountIn, minAmountOut);
}
function _normalizeToV2(address sourceToken, uint256 amountIn) internal {
if (sourceToken == address(legacyCUSDC)) {
IERC20(sourceToken).safeTransferFrom(msg.sender, address(this), amountIn);
legacyCUSDC.burn(amountIn);
cUSDCV2.mint(address(this), amountIn);
return;
}
if (sourceToken == address(cUSDCV2)) {
IERC20(sourceToken).safeTransferFrom(msg.sender, address(this), amountIn);
return;
}
revert UnsupportedSourceToken(sourceToken);
}
}

View File

@@ -0,0 +1,107 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {IAtomicSettlementAdapter} from "./interfaces/IAtomicSettlementAdapter.sol";
interface ICWMultiTokenBridgeL1Settlement {
function calculateFee(address canonicalToken, uint64 destinationChainSelector, address recipient, uint256 amount)
external
view
returns (uint256);
function feeToken() external view returns (address);
function lockAndSend(address canonicalToken, uint64 destinationChainSelector, address recipient, uint256 amount)
external
payable
returns (bytes32 messageId);
}
/**
* @title CWMultiTokenBridgeSettlementAdapter
* @notice Replenishes a fulfiller/vault by forwarding coordinator settlement
* funds into CWMultiTokenBridgeL1.lockAndSend.
*/
contract CWMultiTokenBridgeSettlementAdapter is IAtomicSettlementAdapter {
using SafeERC20 for IERC20;
ICWMultiTokenBridgeL1Settlement public immutable bridge;
uint64 public immutable defaultDestinationSelector;
event CwSettlementSent(
bytes32 indexed obligationId,
bytes32 indexed messageId,
address indexed token,
address recipient,
uint64 destinationSelector,
uint256 amount,
uint256 fee
);
error ZeroAddress();
error FeeTooLow(uint256 requiredFee, uint256 providedFee);
error InvalidSettlementData();
constructor(address bridge_, uint64 defaultDestinationSelector_) {
if (bridge_ == address(0)) revert ZeroAddress();
bridge = ICWMultiTokenBridgeL1Settlement(bridge_);
defaultDestinationSelector = defaultDestinationSelector_;
}
function executeSettlement(
bytes32 obligationId,
address token,
uint256 amount,
address recipient,
bytes calldata data
) external payable returns (bytes32 settlementId) {
(address settlementRecipient, uint64 destinationSelector, uint256 maxFee) =
_decodeSettlementData(recipient, data);
uint256 fee = bridge.calculateFee(token, destinationSelector, settlementRecipient, amount);
if (maxFee != 0 && fee > maxFee) revert FeeTooLow(fee, maxFee);
IERC20(token).forceApprove(address(bridge), amount);
address feeToken = bridge.feeToken();
if (feeToken == address(0)) {
if (msg.value < fee) revert FeeTooLow(fee, msg.value);
settlementId = bridge.lockAndSend{value: fee}(token, destinationSelector, settlementRecipient, amount);
_refundNativeExcess(fee);
} else {
if (IERC20(feeToken).balanceOf(address(this)) < fee) {
revert FeeTooLow(fee, IERC20(feeToken).balanceOf(address(this)));
}
IERC20(feeToken).forceApprove(address(bridge), fee);
settlementId = bridge.lockAndSend(token, destinationSelector, settlementRecipient, amount);
}
emit CwSettlementSent(obligationId, settlementId, token, settlementRecipient, destinationSelector, amount, fee);
}
function _decodeSettlementData(address fallbackRecipient, bytes calldata data)
internal
view
returns (address settlementRecipient, uint64 destinationSelector, uint256 maxFee)
{
if (data.length == 0) {
settlementRecipient = fallbackRecipient;
destinationSelector = defaultDestinationSelector;
} else {
if (data.length != 96) revert InvalidSettlementData();
(settlementRecipient, destinationSelector, maxFee) = abi.decode(data, (address, uint64, uint256));
if (destinationSelector == 0) {
destinationSelector = defaultDestinationSelector;
}
}
if (settlementRecipient == address(0)) revert ZeroAddress();
}
function _refundNativeExcess(uint256 feeUsed) internal {
if (msg.value <= feeUsed) {
return;
}
(bool ok,) = payable(msg.sender).call{value: msg.value - feeUsed}("");
require(ok, "CWMultiTokenBridgeSettlementAdapter: refund failed");
}
}

View File

@@ -0,0 +1,150 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {Pausable} from "@openzeppelin/contracts/utils/Pausable.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
interface IERC3009TransferWithAuthorizationV2 {
function transferWithAuthorization(
address from,
address to,
uint256 value,
uint256 validAfter,
uint256 validBefore,
bytes32 nonce,
uint8 v,
bytes32 r,
bytes32 s
) external;
}
/**
* @title AtomicCwUsdcUsdcPurchaseV2
* @notice Polygon x402 atomic purchase executor with an explorer-friendly `atomicX402Purchase` method name.
* @dev The buyer signs ERC-3009 for cWUSDC_V2 to `treasury`. This contract relays the authorization
* and transfers matching USDC inventory to the buyer in the same transaction.
*/
contract AtomicCwUsdcUsdcPurchaseV2 is Ownable, Pausable, ReentrancyGuard {
using SafeERC20 for IERC20;
IERC3009TransferWithAuthorizationV2 public immutable cWUSDC;
IERC20 public immutable usdc;
address public treasury;
event TreasuryUpdated(address indexed oldTreasury, address indexed newTreasury);
event AtomicX402Purchase(
address indexed payer,
address indexed recipient,
address indexed treasury,
uint256 amount,
bytes32 authorizationNonce
);
event AtomicX402InvoicePurchase(
bytes32 indexed obligationId,
bytes32 indexed invoiceHash,
address indexed payer,
address recipient,
uint256 amount,
bytes32 authorizationNonce
);
error ZeroAddress();
error ZeroAmount();
constructor(address cWUSDC_, address usdc_, address treasury_, address owner_) Ownable(owner_) {
if (cWUSDC_ == address(0) || usdc_ == address(0) || treasury_ == address(0) || owner_ == address(0)) {
revert ZeroAddress();
}
cWUSDC = IERC3009TransferWithAuthorizationV2(cWUSDC_);
usdc = IERC20(usdc_);
treasury = treasury_;
}
function setTreasury(address newTreasury) external onlyOwner {
if (newTreasury == address(0)) revert ZeroAddress();
emit TreasuryUpdated(treasury, newTreasury);
treasury = newTreasury;
}
function pause() external onlyOwner {
_pause();
}
function unpause() external onlyOwner {
_unpause();
}
function atomicX402Purchase(
address payer,
address recipient,
uint256 amount,
uint256 validAfter,
uint256 validBefore,
bytes32 nonce,
uint8 v,
bytes32 r,
bytes32 s
) external nonReentrant whenNotPaused {
_atomicX402Purchase(payer, recipient, amount, validAfter, validBefore, nonce, v, r, s);
}
function purchaseWithAuthorization(
address payer,
address recipient,
uint256 amount,
uint256 validAfter,
uint256 validBefore,
bytes32 nonce,
uint8 v,
bytes32 r,
bytes32 s
) external nonReentrant whenNotPaused {
_atomicX402Purchase(payer, recipient, amount, validAfter, validBefore, nonce, v, r, s);
}
function atomicX402InvoicePurchase(
bytes32 obligationId,
bytes32 invoiceHash,
address payer,
address recipient,
uint256 amount,
uint256 validAfter,
uint256 validBefore,
bytes32 nonce,
uint8 v,
bytes32 r,
bytes32 s
) external nonReentrant whenNotPaused {
_atomicX402Purchase(payer, recipient, amount, validAfter, validBefore, nonce, v, r, s);
emit AtomicX402InvoicePurchase(obligationId, invoiceHash, payer, recipient, amount, nonce);
}
function withdrawToken(address token, address to, uint256 amount) external onlyOwner {
if (token == address(0) || to == address(0)) revert ZeroAddress();
IERC20(token).safeTransfer(to, amount);
}
function _atomicX402Purchase(
address payer,
address recipient,
uint256 amount,
uint256 validAfter,
uint256 validBefore,
bytes32 nonce,
uint8 v,
bytes32 r,
bytes32 s
) internal {
if (payer == address(0) || recipient == address(0)) revert ZeroAddress();
if (amount == 0) revert ZeroAmount();
address currentTreasury = treasury;
cWUSDC.transferWithAuthorization(payer, currentTreasury, amount, validAfter, validBefore, nonce, v, r, s);
usdc.safeTransfer(recipient, amount);
emit AtomicX402Purchase(payer, recipient, currentTreasury, amount, nonce);
}
}

View File

@@ -0,0 +1,73 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {Script, console} from "forge-std/Script.sol";
import "../../../contracts/bridge/atomic/AtomicBridgeCoordinator.sol";
import "../../../contracts/bridge/atomic/AtomicFeePolicy.sol";
import "../../../contracts/bridge/atomic/AtomicSettlementRouter.sol";
import "../../../contracts/bridge/atomic/AtomicTypes.sol";
contract ConfigureAtomicX402PolygonIntent is Script {
uint64 internal constant SOURCE_CHAIN_138 = 138;
uint64 internal constant POLYGON_SELECTOR = 4051577828743386545;
address internal constant CUSDC_V2_CHAIN138 = 0x219522c60e83dEe01FC5b0329d6fA8fD84b9D13d;
address internal constant CWUSDC_V2_POLYGON = 0x7476a64B37732698b9f729fdB373a7B65e9677e9;
bytes32 internal constant SETTLEMENT_MODE = keccak256("CW_POLYGON_CUSDCV2_X402");
function run() external {
uint256 pk = vm.envUint("PRIVATE_KEY");
AtomicBridgeCoordinator coordinator = AtomicBridgeCoordinator(vm.envAddress("ATOMIC_BRIDGE_COORDINATOR"));
AtomicFeePolicy feePolicy = AtomicFeePolicy(vm.envAddress("ATOMIC_FEE_POLICY"));
AtomicSettlementRouter settlementRouter = AtomicSettlementRouter(vm.envAddress("ATOMIC_SETTLEMENT_ROUTER"));
address sourceAdapter = vm.envAddress("ATOMIC_X402_SOURCE_ADAPTER");
address settlementAdapter = vm.envAddress("ATOMIC_CW_SETTLEMENT_ADAPTER");
uint256 maxNotional = vm.envOr("ATOMIC_X402_MAX_NOTIONAL", uint256(110_000e6));
uint16 maxReservedBps = uint16(vm.envOr("ATOMIC_X402_MAX_RESERVED_BPS", uint256(8_000)));
uint256 targetBuffer = vm.envOr("ATOMIC_X402_TARGET_BUFFER", uint256(10_000e6));
uint256 maxSettlementBacklog = vm.envOr("ATOMIC_X402_MAX_SETTLEMENT_BACKLOG", uint256(250_000e6));
uint16 maxOracleDriftBps = uint16(vm.envOr("ATOMIC_X402_MAX_ORACLE_DRIFT_BPS", uint256(100)));
uint256 fulfilmentTimeout = vm.envOr("ATOMIC_X402_FULFILMENT_TIMEOUT", uint256(30 minutes));
uint256 settlementTimeout = vm.envOr("ATOMIC_X402_SETTLEMENT_TIMEOUT", uint256(2 days));
uint16 fulfillerFeeBps = uint16(vm.envOr("ATOMIC_X402_FILLER_FEE_BPS", uint256(100)));
uint16 protocolFeeBps = uint16(vm.envOr("ATOMIC_X402_PROTOCOL_FEE_BPS", uint256(50)));
uint16 bondBps = uint16(vm.envOr("ATOMIC_X402_BOND_BPS", uint256(12_000)));
uint16 slashPenaltyBps = uint16(vm.envOr("ATOMIC_X402_SLASH_PENALTY_BPS", uint256(1_000)));
bytes32 corridorId =
coordinator.getCorridorId(SOURCE_CHAIN_138, POLYGON_SELECTOR, CUSDC_V2_CHAIN138, CWUSDC_V2_POLYGON);
vm.startBroadcast(pk);
coordinator.configureCorridor(
AtomicTypes.CorridorConfig({
enabled: true,
degraded: false,
sourceChain: SOURCE_CHAIN_138,
destinationChain: POLYGON_SELECTOR,
assetIn: CUSDC_V2_CHAIN138,
assetOut: CWUSDC_V2_POLYGON,
maxNotional: maxNotional,
maxReservedBps: maxReservedBps,
targetBuffer: targetBuffer,
maxSettlementBacklog: maxSettlementBacklog,
maxOracleDriftBps: maxOracleDriftBps,
fulfilmentTimeout: fulfilmentTimeout,
settlementTimeout: settlementTimeout,
defaultSettlementMode: SETTLEMENT_MODE
})
);
coordinator.grantRole(coordinator.INTENT_ADAPTER_ROLE(), sourceAdapter);
settlementRouter.setAdapter(SETTLEMENT_MODE, settlementAdapter);
feePolicy.setCorridorPolicy(
corridorId, fulfillerFeeBps, protocolFeeBps, bondBps, slashPenaltyBps, fulfilmentTimeout, settlementTimeout
);
vm.stopBroadcast();
console.log("corridorId");
console.logBytes32(corridorId);
console.log("settlementMode");
console.logBytes32(SETTLEMENT_MODE);
}
}

View 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));
}
}

View 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);
}
}