feat(atomic): add x402 intent bridge adapters
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
119
contracts/bridge/atomic/AtomicX402SourceAdapter.sol
Normal file
119
contracts/bridge/atomic/AtomicX402SourceAdapter.sol
Normal 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);
|
||||
}
|
||||
}
|
||||
107
contracts/bridge/atomic/CWMultiTokenBridgeSettlementAdapter.sol
Normal file
107
contracts/bridge/atomic/CWMultiTokenBridgeSettlementAdapter.sol
Normal 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");
|
||||
}
|
||||
}
|
||||
150
contracts/x402/AtomicCwUsdcUsdcPurchaseV2.sol
Normal file
150
contracts/x402/AtomicCwUsdcUsdcPurchaseV2.sol
Normal 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);
|
||||
}
|
||||
}
|
||||
73
script/bridge/atomic/ConfigureAtomicX402PolygonIntent.s.sol
Normal file
73
script/bridge/atomic/ConfigureAtomicX402PolygonIntent.s.sol
Normal 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);
|
||||
}
|
||||
}
|
||||
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