392 lines
19 KiB
Solidity
392 lines
19 KiB
Solidity
// SPDX-License-Identifier: MIT
|
|
pragma solidity ^0.8.20;
|
|
|
|
import {Test} from "forge-std/Test.sol";
|
|
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
import {
|
|
AaveQuotePushFlashReceiver,
|
|
IAaveExternalUnwinder
|
|
} from "../src/AaveQuotePushFlashReceiver.sol";
|
|
import {AtomicBridgeCoordinator} from "atomic/AtomicBridgeCoordinator.sol";
|
|
import {AtomicFeePolicy} from "atomic/AtomicFeePolicy.sol";
|
|
import {AtomicFulfillerRegistry} from "atomic/AtomicFulfillerRegistry.sol";
|
|
import {AtomicLiquidityVault} from "atomic/AtomicLiquidityVault.sol";
|
|
import {AtomicObligationEscrow} from "atomic/AtomicObligationEscrow.sol";
|
|
import {AtomicSettlementRouter} from "atomic/AtomicSettlementRouter.sol";
|
|
import {AtomicSlashingManager} from "atomic/AtomicSlashingManager.sol";
|
|
import {AtomicTypes} from "atomic/AtomicTypes.sol";
|
|
import {IAtomicSettlementAdapter} from "atomic/interfaces/IAtomicSettlementAdapter.sol";
|
|
import {MockMintableToken} from "repo-test/dbis/MockMintableToken.sol";
|
|
|
|
contract AaveForkMockExternalUnwinder is IAaveExternalUnwinder {
|
|
IERC20 public immutable base;
|
|
IERC20 public immutable quote;
|
|
uint256 public immutable numerator;
|
|
uint256 public immutable denominator;
|
|
|
|
constructor(IERC20 base_, IERC20 quote_, uint256 numerator_, uint256 denominator_) {
|
|
base = base_;
|
|
quote = quote_;
|
|
numerator = numerator_;
|
|
denominator = denominator_;
|
|
}
|
|
|
|
function unwind(address tokenIn, address tokenOut, uint256 amountIn, uint256 minAmountOut, bytes calldata)
|
|
external
|
|
override
|
|
returns (uint256 amountOut)
|
|
{
|
|
require(tokenIn == address(base), "base only");
|
|
require(tokenOut == address(quote), "quote only");
|
|
IERC20(tokenIn).transferFrom(msg.sender, address(this), amountIn);
|
|
amountOut = amountIn * numerator / denominator;
|
|
require(amountOut >= minAmountOut, "min unwind");
|
|
IERC20(address(quote)).transfer(msg.sender, amountOut);
|
|
}
|
|
}
|
|
|
|
contract MockAtomicSettlementAdapter is IAtomicSettlementAdapter {
|
|
address public lastToken;
|
|
uint256 public lastAmount;
|
|
address public lastRecipient;
|
|
bytes32 public lastObligationId;
|
|
|
|
function executeSettlement(
|
|
bytes32 obligationId,
|
|
address token,
|
|
uint256 amount,
|
|
address recipient,
|
|
bytes calldata
|
|
) external payable returns (bytes32 settlementId) {
|
|
lastObligationId = obligationId;
|
|
lastToken = token;
|
|
lastAmount = amount;
|
|
lastRecipient = recipient;
|
|
settlementId = keccak256(abi.encode(obligationId, token, amount, recipient, block.timestamp));
|
|
}
|
|
}
|
|
|
|
contract AaveQuotePushFlashReceiverMainnetForkTest is Test {
|
|
address constant AAVE_POOL_MAINNET = 0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2;
|
|
address constant DODO_PMM_INTEGRATION_MAINNET = 0xa9F284eD010f4F7d7F8F201742b49b9f58e29b84;
|
|
address constant POOL_CWUSDC_USDC = 0x69776fc607e9edA8042e320e7e43f54d06c68f0E;
|
|
address constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;
|
|
address constant CWUSDC = 0x2de5F116bFcE3d0f922d9C8351e0c5Fc24b9284a;
|
|
bytes32 constant MOCK_SETTLEMENT_MODE = keccak256("MOCK_SETTLEMENT_MODE");
|
|
|
|
AaveQuotePushFlashReceiver internal receiver;
|
|
AaveForkMockExternalUnwinder internal unwinder;
|
|
MockMintableToken internal cusdc138;
|
|
MockMintableToken internal bondToken;
|
|
AtomicLiquidityVault internal atomicVault;
|
|
AtomicFulfillerRegistry internal atomicRegistry;
|
|
AtomicFeePolicy internal atomicFeePolicy;
|
|
AtomicObligationEscrow internal atomicEscrow;
|
|
AtomicSettlementRouter internal atomicRouter;
|
|
AtomicSlashingManager internal atomicSlashingManager;
|
|
AtomicBridgeCoordinator internal atomicCoordinator;
|
|
MockAtomicSettlementAdapter internal mockSettlementAdapter;
|
|
bytes32 internal atomicCorridorId;
|
|
address internal destinationRecipient = address(0x138138);
|
|
|
|
function setUp() public {
|
|
string memory rpcUrl = vm.envString("ETHEREUM_MAINNET_RPC");
|
|
vm.createSelectFork(rpcUrl);
|
|
|
|
receiver = new AaveQuotePushFlashReceiver(AAVE_POOL_MAINNET);
|
|
unwinder = new AaveForkMockExternalUnwinder(IERC20(CWUSDC), IERC20(USDC), 130, 100);
|
|
deal(USDC, address(unwinder), 50_000_000_000);
|
|
|
|
cusdc138 = new MockMintableToken("Chain 138 USDC", "cUSDC", 6, address(this));
|
|
bondToken = new MockMintableToken("Atomic Bond", "aBOND", 6, address(this));
|
|
atomicVault = new AtomicLiquidityVault(address(this));
|
|
atomicRegistry = new AtomicFulfillerRegistry(address(bondToken), address(this));
|
|
atomicFeePolicy = new AtomicFeePolicy(address(this));
|
|
atomicEscrow = new AtomicObligationEscrow(address(this));
|
|
atomicRouter = new AtomicSettlementRouter(address(this));
|
|
atomicSlashingManager = new AtomicSlashingManager(address(atomicRegistry), address(this));
|
|
mockSettlementAdapter = new MockAtomicSettlementAdapter();
|
|
atomicCoordinator = new AtomicBridgeCoordinator(
|
|
address(atomicVault),
|
|
address(atomicRegistry),
|
|
address(atomicEscrow),
|
|
address(atomicRouter),
|
|
address(atomicFeePolicy),
|
|
address(atomicSlashingManager),
|
|
address(this),
|
|
address(this)
|
|
);
|
|
|
|
atomicVault.grantRole(atomicVault.COORDINATOR_ROLE(), address(atomicCoordinator));
|
|
atomicVault.grantRole(atomicVault.RECONCILER_ROLE(), address(atomicCoordinator));
|
|
atomicRegistry.grantRole(atomicRegistry.COORDINATOR_ROLE(), address(atomicCoordinator));
|
|
atomicRegistry.grantRole(atomicRegistry.SLASHER_ROLE(), address(atomicSlashingManager));
|
|
atomicEscrow.grantRole(atomicEscrow.COORDINATOR_ROLE(), address(atomicCoordinator));
|
|
atomicRouter.grantRole(atomicRouter.COORDINATOR_ROLE(), address(atomicCoordinator));
|
|
atomicSlashingManager.grantRole(atomicSlashingManager.COORDINATOR_ROLE(), address(atomicCoordinator));
|
|
atomicRouter.setAdapter(MOCK_SETTLEMENT_MODE, address(mockSettlementAdapter));
|
|
|
|
atomicCorridorId = atomicCoordinator.getCorridorId(1, 138, CWUSDC, address(cusdc138));
|
|
atomicCoordinator.configureCorridor(
|
|
AtomicTypes.CorridorConfig({
|
|
enabled: true,
|
|
degraded: false,
|
|
sourceChain: 1,
|
|
destinationChain: 138,
|
|
assetIn: CWUSDC,
|
|
assetOut: address(cusdc138),
|
|
maxNotional: 10_000_000,
|
|
maxReservedBps: 8_000,
|
|
targetBuffer: 100_000,
|
|
maxSettlementBacklog: 5_000_000,
|
|
maxOracleDriftBps: 500,
|
|
fulfilmentTimeout: 1 days,
|
|
settlementTimeout: 2 days,
|
|
defaultSettlementMode: MOCK_SETTLEMENT_MODE
|
|
})
|
|
);
|
|
atomicFeePolicy.setCorridorPolicy(atomicCorridorId, 25, 10, 12_000, 500, 1 days, 2 days);
|
|
atomicVault.setTargetBuffer(atomicCorridorId, address(cusdc138), 100_000);
|
|
cusdc138.mint(address(this), 5_000_000);
|
|
cusdc138.approve(address(atomicVault), type(uint256).max);
|
|
atomicVault.fundCorridor(atomicCorridorId, address(cusdc138), 5_000_000);
|
|
|
|
bondToken.mint(address(receiver), 5_000_000);
|
|
vm.startPrank(address(receiver));
|
|
bondToken.approve(address(atomicRegistry), type(uint256).max);
|
|
atomicRegistry.depositBond(3_000_000);
|
|
vm.stopPrank();
|
|
atomicRegistry.setFulfillerActive(address(receiver), true);
|
|
atomicRegistry.setCorridorAuthorization(address(receiver), atomicCorridorId, true);
|
|
}
|
|
|
|
function testFork_aaveQuotePush_usesRealAaveAndRealMainnetPmm() public {
|
|
uint256 amount = 2_964_298;
|
|
uint256 receiverQuoteBefore = IERC20(USDC).balanceOf(address(receiver));
|
|
uint256 poolBaseBefore = IERC20(CWUSDC).balanceOf(POOL_CWUSDC_USDC);
|
|
uint256 poolQuoteBefore = IERC20(USDC).balanceOf(POOL_CWUSDC_USDC);
|
|
|
|
AaveQuotePushFlashReceiver.QuotePushParams memory p = AaveQuotePushFlashReceiver.QuotePushParams({
|
|
integration: DODO_PMM_INTEGRATION_MAINNET,
|
|
pmmPool: POOL_CWUSDC_USDC,
|
|
baseToken: CWUSDC,
|
|
externalUnwinder: address(unwinder),
|
|
minOutPmm: 2_800_000,
|
|
minOutUnwind: amount + 1_483,
|
|
unwindData: bytes(""),
|
|
atomicBridge: AaveQuotePushFlashReceiver.AtomicBridgeParams({
|
|
coordinator: address(0),
|
|
sourceChain: 0,
|
|
destinationChain: 0,
|
|
destinationAsset: address(0),
|
|
bridgeAmount: 0,
|
|
minDestinationAmount: 0,
|
|
destinationRecipient: address(0),
|
|
destinationDeadline: 0,
|
|
routeId: bytes32(0),
|
|
settlementMode: bytes32(0),
|
|
submitCommitment: false
|
|
})
|
|
});
|
|
|
|
receiver.flashQuotePush(USDC, amount, p);
|
|
|
|
uint256 receiverQuoteAfter = IERC20(USDC).balanceOf(address(receiver));
|
|
uint256 poolBaseAfter = IERC20(CWUSDC).balanceOf(POOL_CWUSDC_USDC);
|
|
uint256 poolQuoteAfter = IERC20(USDC).balanceOf(POOL_CWUSDC_USDC);
|
|
uint256 actualBaseOut = poolBaseBefore - poolBaseAfter;
|
|
uint256 actualQuoteIntoPool = poolQuoteAfter - poolQuoteBefore;
|
|
uint256 actualSurplus = receiverQuoteAfter - receiverQuoteBefore;
|
|
uint256 premium = _aavePremium(amount);
|
|
(uint256 predictedBaseOut, uint256 predictedUnwindOut, uint256 predictedSurplus) =
|
|
_predictQuotePush(poolBaseBefore, poolQuoteBefore, amount, 3, 130, 100, 0, premium);
|
|
uint256 predictedNetQuoteIn = _netQuoteIn(amount, 3);
|
|
|
|
assertGt(receiverQuoteAfter, receiverQuoteBefore, "receiver retains surplus");
|
|
assertEq(IERC20(CWUSDC).balanceOf(address(receiver)), 0, "base fully unwound");
|
|
assertLt(poolBaseAfter, poolBaseBefore, "pool base decreased");
|
|
assertGt(poolQuoteAfter, poolQuoteBefore, "pool quote increased");
|
|
_assertWithinOnePercent(actualBaseOut, predictedBaseOut, "baseOut");
|
|
_assertWithinOnePercent(actualQuoteIntoPool, predictedNetQuoteIn, "netQuoteIn");
|
|
_assertWithinOnePercent(actualSurplus, predictedSurplus, "surplus");
|
|
assertApproxEqAbs(predictedUnwindOut, actualSurplus + amount + premium, 2, "unwindOut");
|
|
}
|
|
|
|
function testFork_aaveQuotePush_atomicCorridorFulfillment_1_to_138_cwusdc_to_cusdc() public {
|
|
uint256 amount = 2_964_298;
|
|
uint256 bridgeAmount = 500_000;
|
|
uint256 destinationRecipientBefore = cusdc138.balanceOf(destinationRecipient);
|
|
uint256 poolBaseBefore = IERC20(CWUSDC).balanceOf(POOL_CWUSDC_USDC);
|
|
uint256 poolQuoteBefore = IERC20(USDC).balanceOf(POOL_CWUSDC_USDC);
|
|
|
|
AaveQuotePushFlashReceiver.QuotePushParams memory p = AaveQuotePushFlashReceiver.QuotePushParams({
|
|
integration: DODO_PMM_INTEGRATION_MAINNET,
|
|
pmmPool: POOL_CWUSDC_USDC,
|
|
baseToken: CWUSDC,
|
|
externalUnwinder: address(unwinder),
|
|
minOutPmm: 2_800_000,
|
|
minOutUnwind: amount + 1_483,
|
|
unwindData: bytes(""),
|
|
atomicBridge: AaveQuotePushFlashReceiver.AtomicBridgeParams({
|
|
coordinator: address(atomicCoordinator),
|
|
sourceChain: 1,
|
|
destinationChain: 138,
|
|
destinationAsset: address(cusdc138),
|
|
bridgeAmount: bridgeAmount,
|
|
minDestinationAmount: bridgeAmount,
|
|
destinationRecipient: destinationRecipient,
|
|
destinationDeadline: block.timestamp + 1 hours,
|
|
routeId: atomicCorridorId,
|
|
settlementMode: bytes32(0),
|
|
submitCommitment: true
|
|
})
|
|
});
|
|
|
|
uint256 receiverQuoteBefore = IERC20(USDC).balanceOf(address(receiver));
|
|
receiver.flashQuotePush(USDC, amount, p);
|
|
|
|
uint256 receiverQuoteAfter = IERC20(USDC).balanceOf(address(receiver));
|
|
uint256 poolBaseAfter = IERC20(CWUSDC).balanceOf(POOL_CWUSDC_USDC);
|
|
uint256 poolQuoteAfter = IERC20(USDC).balanceOf(POOL_CWUSDC_USDC);
|
|
uint256 actualBaseOut = poolBaseBefore - poolBaseAfter;
|
|
uint256 actualQuoteIntoPool = poolQuoteAfter - poolQuoteBefore;
|
|
uint256 actualSurplus = receiverQuoteAfter - receiverQuoteBefore;
|
|
uint256 premium = _aavePremium(amount);
|
|
(uint256 predictedBaseOut, uint256 predictedUnwindOut, uint256 predictedSurplus) =
|
|
_predictQuotePush(poolBaseBefore, poolQuoteBefore, amount, 3, 130, 100, bridgeAmount, premium);
|
|
uint256 predictedNetQuoteIn = _netQuoteIn(amount, 3);
|
|
assertGt(receiverQuoteAfter, receiverQuoteBefore, "receiver still retains quote surplus");
|
|
assertEq(cusdc138.balanceOf(destinationRecipient), destinationRecipientBefore + bridgeAmount, "destination funded");
|
|
assertEq(IERC20(CWUSDC).balanceOf(address(receiver)), 0, "remaining base fully unwound");
|
|
_assertWithinOnePercent(actualBaseOut, predictedBaseOut, "atomic baseOut");
|
|
_assertWithinOnePercent(actualQuoteIntoPool, predictedNetQuoteIn, "atomic netQuoteIn");
|
|
_assertWithinOnePercent(actualSurplus, predictedSurplus, "atomic surplus");
|
|
assertApproxEqAbs(predictedUnwindOut, actualSurplus + amount + premium, 2, "atomic unwindOut");
|
|
|
|
AtomicTypes.CorridorLiquidityState memory state =
|
|
atomicVault.getCorridorLiquidityState(atomicCorridorId, address(cusdc138));
|
|
assertEq(state.settlementBacklog, bridgeAmount, "backlog increased by delivered amount");
|
|
assertEq(state.totalLiquidity, 5_000_000 - bridgeAmount, "vault liquidity debited on immediate fulfillment");
|
|
}
|
|
|
|
function testFork_aaveQuotePush_atomicCorridorSettlementConfirmation_1_to_138() public {
|
|
uint256 amount = 2_964_298;
|
|
uint256 bridgeAmount = 500_000;
|
|
uint256 deadline = block.timestamp + 1 hours;
|
|
uint256 receiverBondBefore = atomicRegistry.availableBond(address(receiver));
|
|
uint256 treasuryBaseBefore = IERC20(CWUSDC).balanceOf(address(this));
|
|
uint256 receiverBaseBefore = IERC20(CWUSDC).balanceOf(address(receiver));
|
|
|
|
AaveQuotePushFlashReceiver.QuotePushParams memory p = AaveQuotePushFlashReceiver.QuotePushParams({
|
|
integration: DODO_PMM_INTEGRATION_MAINNET,
|
|
pmmPool: POOL_CWUSDC_USDC,
|
|
baseToken: CWUSDC,
|
|
externalUnwinder: address(unwinder),
|
|
minOutPmm: 2_800_000,
|
|
minOutUnwind: amount + 1_483,
|
|
unwindData: bytes(""),
|
|
atomicBridge: AaveQuotePushFlashReceiver.AtomicBridgeParams({
|
|
coordinator: address(atomicCoordinator),
|
|
sourceChain: 1,
|
|
destinationChain: 138,
|
|
destinationAsset: address(cusdc138),
|
|
bridgeAmount: bridgeAmount,
|
|
minDestinationAmount: bridgeAmount,
|
|
destinationRecipient: destinationRecipient,
|
|
destinationDeadline: deadline,
|
|
routeId: atomicCorridorId,
|
|
settlementMode: bytes32(0),
|
|
submitCommitment: true
|
|
})
|
|
});
|
|
|
|
receiver.flashQuotePush(USDC, amount, p);
|
|
bytes32 obligationId = _deriveObligationId(bridgeAmount, deadline);
|
|
|
|
AtomicTypes.AtomicObligation memory fulfilled = atomicCoordinator.getObligation(obligationId);
|
|
assertEq(uint8(fulfilled.status), uint8(AtomicTypes.ObligationStatus.Fulfilled), "obligation fulfilled");
|
|
|
|
atomicCoordinator.initiateSettlement(obligationId, abi.encodePacked(bytes32(uint256(138))));
|
|
|
|
AtomicTypes.AtomicObligation memory pending = atomicCoordinator.getObligation(obligationId);
|
|
assertEq(uint8(pending.status), uint8(AtomicTypes.ObligationStatus.SettlementPending), "obligation pending");
|
|
assertEq(mockSettlementAdapter.lastObligationId(), obligationId, "adapter saw obligation");
|
|
assertEq(mockSettlementAdapter.lastToken(), CWUSDC, "adapter token");
|
|
assertEq(mockSettlementAdapter.lastRecipient(), destinationRecipient, "adapter recipient");
|
|
|
|
uint256 expectedFulfillerFee = (bridgeAmount * 25) / 10_000;
|
|
uint256 expectedProtocolFee = (bridgeAmount * 10) / 10_000;
|
|
uint256 expectedSettlementAmount = bridgeAmount - expectedFulfillerFee - expectedProtocolFee;
|
|
assertEq(mockSettlementAdapter.lastAmount(), expectedSettlementAmount, "adapter amount");
|
|
assertEq(IERC20(CWUSDC).balanceOf(address(this)), treasuryBaseBefore + expectedProtocolFee, "protocol fee received");
|
|
assertEq(
|
|
IERC20(CWUSDC).balanceOf(address(receiver)),
|
|
receiverBaseBefore + expectedFulfillerFee,
|
|
"fulfiller fee received"
|
|
);
|
|
|
|
cusdc138.mint(address(this), bridgeAmount);
|
|
cusdc138.approve(address(atomicVault), bridgeAmount);
|
|
atomicCoordinator.confirmSettlement(obligationId, bridgeAmount);
|
|
|
|
AtomicTypes.AtomicObligation memory settled = atomicCoordinator.getObligation(obligationId);
|
|
assertEq(uint8(settled.status), uint8(AtomicTypes.ObligationStatus.Settled), "obligation settled");
|
|
|
|
AtomicTypes.CorridorLiquidityState memory state =
|
|
atomicVault.getCorridorLiquidityState(atomicCorridorId, address(cusdc138));
|
|
assertEq(state.settlementBacklog, 0, "backlog cleared");
|
|
assertEq(state.totalLiquidity, 5_000_000, "vault replenished");
|
|
assertEq(atomicRegistry.availableBond(address(receiver)), receiverBondBefore, "bond released");
|
|
}
|
|
|
|
function _deriveObligationId(uint256 bridgeAmount, uint256 deadline) internal view returns (bytes32) {
|
|
bytes32 intentId = keccak256(
|
|
abi.encode(
|
|
block.chainid,
|
|
address(receiver),
|
|
uint256(1),
|
|
atomicCorridorId,
|
|
bridgeAmount,
|
|
bridgeAmount,
|
|
deadline,
|
|
atomicCorridorId
|
|
)
|
|
);
|
|
return keccak256(abi.encode(intentId, destinationRecipient));
|
|
}
|
|
|
|
function _predictQuotePush(
|
|
uint256 baseReserve,
|
|
uint256 quoteReserve,
|
|
uint256 grossQuoteIn,
|
|
uint256 lpFeeBps,
|
|
uint256 unwindNumerator,
|
|
uint256 unwindDenominator,
|
|
uint256 bridgeAmount,
|
|
uint256 premium
|
|
) internal pure returns (uint256 predictedBaseOut, uint256 predictedUnwindOut, uint256 predictedSurplus) {
|
|
uint256 netQuoteIn = (grossQuoteIn * (10_000 - lpFeeBps)) / 10_000;
|
|
predictedBaseOut = (netQuoteIn * baseReserve) / (quoteReserve + netQuoteIn);
|
|
uint256 remainingBase = predictedBaseOut - bridgeAmount;
|
|
predictedUnwindOut = (remainingBase * unwindNumerator) / unwindDenominator;
|
|
predictedSurplus = predictedUnwindOut - grossQuoteIn - premium;
|
|
}
|
|
|
|
function _netQuoteIn(uint256 grossQuoteIn, uint256 lpFeeBps) internal pure returns (uint256) {
|
|
return (grossQuoteIn * (10_000 - lpFeeBps)) / 10_000;
|
|
}
|
|
|
|
function _assertWithinOnePercent(uint256 actual, uint256 expected, string memory label) internal pure {
|
|
if (expected == 0) {
|
|
require(actual == 0, label);
|
|
return;
|
|
}
|
|
uint256 diff = actual > expected ? actual - expected : expected - actual;
|
|
require(diff * 10_000 <= expected * 100, label);
|
|
}
|
|
|
|
function _aavePremium(uint256 amount) internal pure returns (uint256) {
|
|
return (amount * 5) / 10_000;
|
|
}
|
|
}
|