@bananapus/suckers-v6 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +422 -0
- package/SECURITY.md +55 -0
- package/SKILLS.md +163 -0
- package/deployments/nana-suckers-v5/arbitrum/JBArbitrumSucker.json +1425 -0
- package/deployments/nana-suckers-v5/arbitrum/JBArbitrumSuckerDeployer.json +391 -0
- package/deployments/nana-suckers-v5/arbitrum/JBCCIPSucker.json +1479 -0
- package/deployments/nana-suckers-v5/arbitrum/JBCCIPSuckerDeployer.json +433 -0
- package/deployments/nana-suckers-v5/arbitrum/JBCCIPSuckerDeployer_1.json +433 -0
- package/deployments/nana-suckers-v5/arbitrum/JBCCIPSuckerDeployer_2.json +433 -0
- package/deployments/nana-suckers-v5/arbitrum/JBCCIPSucker_1.json +1479 -0
- package/deployments/nana-suckers-v5/arbitrum/JBCCIPSucker_2.json +1479 -0
- package/deployments/nana-suckers-v5/arbitrum/JBSuckerRegistry.json +690 -0
- package/deployments/nana-suckers-v5/arbitrum_sepolia/JBArbitrumSucker.json +1425 -0
- package/deployments/nana-suckers-v5/arbitrum_sepolia/JBArbitrumSuckerDeployer.json +391 -0
- package/deployments/nana-suckers-v5/arbitrum_sepolia/JBCCIPSucker.json +1479 -0
- package/deployments/nana-suckers-v5/arbitrum_sepolia/JBCCIPSuckerDeployer.json +433 -0
- package/deployments/nana-suckers-v5/arbitrum_sepolia/JBCCIPSuckerDeployer_1.json +433 -0
- package/deployments/nana-suckers-v5/arbitrum_sepolia/JBCCIPSuckerDeployer_2.json +433 -0
- package/deployments/nana-suckers-v5/arbitrum_sepolia/JBCCIPSucker_1.json +1479 -0
- package/deployments/nana-suckers-v5/arbitrum_sepolia/JBCCIPSucker_2.json +1479 -0
- package/deployments/nana-suckers-v5/arbitrum_sepolia/JBSuckerRegistry.json +690 -0
- package/deployments/nana-suckers-v5/base/JBBaseSucker.json +1389 -0
- package/deployments/nana-suckers-v5/base/JBBaseSuckerDeployer.json +376 -0
- package/deployments/nana-suckers-v5/base/JBCCIPSucker.json +1483 -0
- package/deployments/nana-suckers-v5/base/JBCCIPSuckerDeployer.json +436 -0
- package/deployments/nana-suckers-v5/base/JBCCIPSuckerDeployer_1.json +436 -0
- package/deployments/nana-suckers-v5/base/JBCCIPSuckerDeployer_2.json +436 -0
- package/deployments/nana-suckers-v5/base/JBCCIPSucker_1.json +1483 -0
- package/deployments/nana-suckers-v5/base/JBCCIPSucker_2.json +1483 -0
- package/deployments/nana-suckers-v5/base/JBSuckerRegistry.json +694 -0
- package/deployments/nana-suckers-v5/base_sepolia/JBBaseSucker.json +1389 -0
- package/deployments/nana-suckers-v5/base_sepolia/JBBaseSuckerDeployer.json +376 -0
- package/deployments/nana-suckers-v5/base_sepolia/JBCCIPSucker.json +1483 -0
- package/deployments/nana-suckers-v5/base_sepolia/JBCCIPSuckerDeployer.json +436 -0
- package/deployments/nana-suckers-v5/base_sepolia/JBCCIPSuckerDeployer_1.json +436 -0
- package/deployments/nana-suckers-v5/base_sepolia/JBCCIPSuckerDeployer_2.json +436 -0
- package/deployments/nana-suckers-v5/base_sepolia/JBCCIPSucker_1.json +1483 -0
- package/deployments/nana-suckers-v5/base_sepolia/JBCCIPSucker_2.json +1483 -0
- package/deployments/nana-suckers-v5/base_sepolia/JBSuckerRegistry.json +694 -0
- package/deployments/nana-suckers-v5/ethereum/JBArbitrumSucker.json +1429 -0
- package/deployments/nana-suckers-v5/ethereum/JBArbitrumSuckerDeployer.json +394 -0
- package/deployments/nana-suckers-v5/ethereum/JBBaseSucker.json +1389 -0
- package/deployments/nana-suckers-v5/ethereum/JBBaseSuckerDeployer.json +376 -0
- package/deployments/nana-suckers-v5/ethereum/JBCCIPSucker.json +1483 -0
- package/deployments/nana-suckers-v5/ethereum/JBCCIPSuckerDeployer.json +436 -0
- package/deployments/nana-suckers-v5/ethereum/JBCCIPSuckerDeployer_1.json +436 -0
- package/deployments/nana-suckers-v5/ethereum/JBCCIPSuckerDeployer_2.json +436 -0
- package/deployments/nana-suckers-v5/ethereum/JBCCIPSucker_1.json +1483 -0
- package/deployments/nana-suckers-v5/ethereum/JBCCIPSucker_2.json +1483 -0
- package/deployments/nana-suckers-v5/ethereum/JBOptimismSucker.json +1389 -0
- package/deployments/nana-suckers-v5/ethereum/JBOptimismSuckerDeployer.json +376 -0
- package/deployments/nana-suckers-v5/ethereum/JBSuckerRegistry.json +694 -0
- package/deployments/nana-suckers-v5/optimism/JBCCIPSucker.json +1479 -0
- package/deployments/nana-suckers-v5/optimism/JBCCIPSuckerDeployer.json +433 -0
- package/deployments/nana-suckers-v5/optimism/JBCCIPSuckerDeployer_1.json +433 -0
- package/deployments/nana-suckers-v5/optimism/JBCCIPSuckerDeployer_2.json +433 -0
- package/deployments/nana-suckers-v5/optimism/JBCCIPSucker_1.json +1479 -0
- package/deployments/nana-suckers-v5/optimism/JBCCIPSucker_2.json +1479 -0
- package/deployments/nana-suckers-v5/optimism/JBOptimismSucker.json +1385 -0
- package/deployments/nana-suckers-v5/optimism/JBOptimismSuckerDeployer.json +373 -0
- package/deployments/nana-suckers-v5/optimism/JBSuckerRegistry.json +690 -0
- package/deployments/nana-suckers-v5/optimism_sepolia/JBCCIPSucker.json +1483 -0
- package/deployments/nana-suckers-v5/optimism_sepolia/JBCCIPSuckerDeployer.json +436 -0
- package/deployments/nana-suckers-v5/optimism_sepolia/JBCCIPSuckerDeployer_1.json +436 -0
- package/deployments/nana-suckers-v5/optimism_sepolia/JBCCIPSuckerDeployer_2.json +436 -0
- package/deployments/nana-suckers-v5/optimism_sepolia/JBCCIPSucker_1.json +1483 -0
- package/deployments/nana-suckers-v5/optimism_sepolia/JBCCIPSucker_2.json +1483 -0
- package/deployments/nana-suckers-v5/optimism_sepolia/JBOptimismSucker.json +1389 -0
- package/deployments/nana-suckers-v5/optimism_sepolia/JBOptimismSuckerDeployer.json +376 -0
- package/deployments/nana-suckers-v5/optimism_sepolia/JBSuckerRegistry.json +694 -0
- package/deployments/nana-suckers-v5/sepolia/JBArbitrumSucker.json +1429 -0
- package/deployments/nana-suckers-v5/sepolia/JBArbitrumSuckerDeployer.json +394 -0
- package/deployments/nana-suckers-v5/sepolia/JBBaseSucker.json +1389 -0
- package/deployments/nana-suckers-v5/sepolia/JBBaseSuckerDeployer.json +376 -0
- package/deployments/nana-suckers-v5/sepolia/JBCCIPSucker.json +1483 -0
- package/deployments/nana-suckers-v5/sepolia/JBCCIPSuckerDeployer.json +436 -0
- package/deployments/nana-suckers-v5/sepolia/JBCCIPSuckerDeployer_1.json +436 -0
- package/deployments/nana-suckers-v5/sepolia/JBCCIPSuckerDeployer_2.json +436 -0
- package/deployments/nana-suckers-v5/sepolia/JBCCIPSucker_1.json +1483 -0
- package/deployments/nana-suckers-v5/sepolia/JBCCIPSucker_2.json +1483 -0
- package/deployments/nana-suckers-v5/sepolia/JBOptimismSucker.json +1389 -0
- package/deployments/nana-suckers-v5/sepolia/JBOptimismSuckerDeployer.json +376 -0
- package/deployments/nana-suckers-v5/sepolia/JBSuckerRegistry.json +694 -0
- package/foundry.lock +11 -0
- package/foundry.toml +22 -0
- package/package.json +33 -0
- package/remappings.txt +1 -0
- package/script/Deploy.s.sol +506 -0
- package/script/helpers/SuckerDeploymentLib.sol +97 -0
- package/slither-ci.config.json +10 -0
- package/sphinx.lock +476 -0
- package/src/JBArbitrumSucker.sol +311 -0
- package/src/JBBaseSucker.sol +41 -0
- package/src/JBCCIPSucker.sol +303 -0
- package/src/JBOptimismSucker.sol +143 -0
- package/src/JBSucker.sol +1159 -0
- package/src/JBSuckerRegistry.sol +262 -0
- package/src/deployers/JBArbitrumSuckerDeployer.sol +86 -0
- package/src/deployers/JBBaseSuckerDeployer.sol +26 -0
- package/src/deployers/JBCCIPSuckerDeployer.sol +88 -0
- package/src/deployers/JBOptimismSuckerDeployer.sol +82 -0
- package/src/deployers/JBSuckerDeployer.sol +147 -0
- package/src/enums/JBAddToBalanceMode.sol +11 -0
- package/src/enums/JBLayer.sol +8 -0
- package/src/enums/JBSuckerState.sol +14 -0
- package/src/interfaces/IArbGatewayRouter.sol +11 -0
- package/src/interfaces/IArbL1GatewayRouter.sol +17 -0
- package/src/interfaces/IArbL2GatewayRouter.sol +14 -0
- package/src/interfaces/ICCIPRouter.sol +11 -0
- package/src/interfaces/IJBArbitrumSucker.sol +13 -0
- package/src/interfaces/IJBArbitrumSuckerDeployer.sol +12 -0
- package/src/interfaces/IJBCCIPSuckerDeployer.sol +15 -0
- package/src/interfaces/IJBOpSuckerDeployer.sol +11 -0
- package/src/interfaces/IJBOptimismSucker.sol +10 -0
- package/src/interfaces/IJBSucker.sol +144 -0
- package/src/interfaces/IJBSuckerDeployer.sol +40 -0
- package/src/interfaces/IJBSuckerExtended.sol +22 -0
- package/src/interfaces/IJBSuckerRegistry.sol +75 -0
- package/src/interfaces/IOPMessenger.sol +18 -0
- package/src/interfaces/IOPStandardBridge.sol +29 -0
- package/src/interfaces/IWrappedNativeToken.sol +13 -0
- package/src/libraries/ARBAddresses.sol +17 -0
- package/src/libraries/ARBChains.sol +11 -0
- package/src/libraries/CCIPHelper.sol +136 -0
- package/src/structs/JBClaim.sol +13 -0
- package/src/structs/JBInboxTreeRoot.sol +12 -0
- package/src/structs/JBLeaf.sol +14 -0
- package/src/structs/JBMessageRoot.sol +16 -0
- package/src/structs/JBOutboxTree.sol +18 -0
- package/src/structs/JBRemoteToken.sol +17 -0
- package/src/structs/JBSuckerDeployerConfig.sol +12 -0
- package/src/structs/JBSuckersPair.sol +11 -0
- package/src/structs/JBTokenMapping.sol +13 -0
- package/src/utils/MerkleLib.sol +1020 -0
- package/test/Fork.t.sol +514 -0
- package/test/InteropCompat.t.sol +676 -0
- package/test/SuckerAttacks.t.sol +509 -0
- package/test/SuckerDeepAttacks.t.sol +1563 -0
- package/test/mocks/ERC20Mock.sol +36 -0
- package/test/mocks/MockMessenger.sol +42 -0
- package/test/unit/arb.t.sol +28 -0
- package/test/unit/ccip_native_interop.t.sol +719 -0
- package/test/unit/ccip_refund.t.sol +234 -0
- package/test/unit/deployer.t.sol +475 -0
- package/test/unit/emergency.t.sol +305 -0
- package/test/unit/merkle.t.sol +212 -0
- package/test/unit/multi_chain_evolution.t.sol +622 -0
- package/test/unit/registry.t.sol +26 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.23;
|
|
3
|
+
|
|
4
|
+
import "forge-std/Test.sol";
|
|
5
|
+
|
|
6
|
+
import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
|
|
7
|
+
import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
|
|
8
|
+
import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
|
|
9
|
+
import {IJBTokens} from "@bananapus/core-v6/src/interfaces/IJBTokens.sol";
|
|
10
|
+
import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
|
|
11
|
+
import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
|
|
12
|
+
import {LibClone} from "solady/src/utils/LibClone.sol";
|
|
13
|
+
|
|
14
|
+
import {JBCCIPSucker} from "../../src/JBCCIPSucker.sol";
|
|
15
|
+
import {JBSucker} from "../../src/JBSucker.sol";
|
|
16
|
+
import {JBCCIPSuckerDeployer} from "../../src/deployers/JBCCIPSuckerDeployer.sol";
|
|
17
|
+
import {JBAddToBalanceMode} from "../../src/enums/JBAddToBalanceMode.sol";
|
|
18
|
+
import {IJBCCIPSuckerDeployer} from "../../src/interfaces/IJBCCIPSuckerDeployer.sol";
|
|
19
|
+
import {ICCIPRouter} from "../../src/interfaces/ICCIPRouter.sol";
|
|
20
|
+
import {IRouterClient} from "@chainlink/contracts-ccip/src/v0.8/ccip/interfaces/IRouterClient.sol";
|
|
21
|
+
import {JBRemoteToken} from "../../src/structs/JBRemoteToken.sol";
|
|
22
|
+
import {JBTokenMapping} from "../../src/structs/JBTokenMapping.sol";
|
|
23
|
+
import {MerkleLib} from "../../src/utils/MerkleLib.sol";
|
|
24
|
+
|
|
25
|
+
/// @notice Harness that exposes internal state for testing.
|
|
26
|
+
contract CCIPSuckerHarness is JBCCIPSucker {
|
|
27
|
+
using MerkleLib for MerkleLib.Tree;
|
|
28
|
+
|
|
29
|
+
constructor(
|
|
30
|
+
JBCCIPSuckerDeployer deployer,
|
|
31
|
+
IJBDirectory directory,
|
|
32
|
+
IJBTokens tokens,
|
|
33
|
+
IJBPermissions permissions,
|
|
34
|
+
JBAddToBalanceMode addToBalanceMode,
|
|
35
|
+
address trusted_forwarder
|
|
36
|
+
)
|
|
37
|
+
JBCCIPSucker(deployer, directory, tokens, permissions, addToBalanceMode, trusted_forwarder)
|
|
38
|
+
{}
|
|
39
|
+
|
|
40
|
+
/// @notice Directly insert a leaf into the outbox tree for testing.
|
|
41
|
+
function test_insertIntoTree(
|
|
42
|
+
uint256 projectTokenCount,
|
|
43
|
+
address token,
|
|
44
|
+
uint256 terminalTokenAmount,
|
|
45
|
+
bytes32 beneficiary
|
|
46
|
+
)
|
|
47
|
+
external
|
|
48
|
+
{
|
|
49
|
+
_insertIntoTree(projectTokenCount, token, terminalTokenAmount, beneficiary);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/// @notice Set the remote token mapping for testing.
|
|
53
|
+
function test_setRemoteToken(address localToken, JBRemoteToken memory remoteToken) external {
|
|
54
|
+
_remoteTokenFor[localToken] = remoteToken;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/// @notice Get outbox balance.
|
|
58
|
+
function test_getOutboxBalance(address token) external view returns (uint256) {
|
|
59
|
+
return _outboxOf[token].balance;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/// @notice Non-payable contract that calls toRemote — simulates a contract caller that can't receive refunds.
|
|
64
|
+
contract NonPayableCaller {
|
|
65
|
+
function callToRemote(address sucker, address token) external payable {
|
|
66
|
+
JBCCIPSucker(payable(sucker)).toRemote{value: msg.value}(token);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/// @notice Payable contract that calls toRemote — receives refunds normally.
|
|
71
|
+
contract PayableCaller {
|
|
72
|
+
function callToRemote(address sucker, address token) external payable {
|
|
73
|
+
JBCCIPSucker(payable(sucker)).toRemote{value: msg.value}(token);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
receive() external payable {}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/// @title CCIPRefundTest
|
|
80
|
+
/// @notice Tests for M-2 fix: CCIP refund failure emits event instead of reverting.
|
|
81
|
+
contract CCIPRefundTest is Test {
|
|
82
|
+
address constant DIRECTORY = address(0x1001);
|
|
83
|
+
address constant PERMISSIONS = address(0x1002);
|
|
84
|
+
address constant TOKENS = address(0x1003);
|
|
85
|
+
address constant MOCK_ROUTER = address(0x2001);
|
|
86
|
+
address constant FORWARDER = address(0x3001);
|
|
87
|
+
address constant PROJECT = address(0x4001);
|
|
88
|
+
|
|
89
|
+
uint256 constant PROJECT_ID = 42;
|
|
90
|
+
uint256 constant REMOTE_CHAIN_ID = 137;
|
|
91
|
+
uint64 constant REMOTE_CHAIN_SELECTOR = 4_051_577_828_743_386_545;
|
|
92
|
+
address constant TOKEN = address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE);
|
|
93
|
+
|
|
94
|
+
CCIPSuckerHarness sucker;
|
|
95
|
+
|
|
96
|
+
function setUp() public {
|
|
97
|
+
// Mock the deployer interface for the constructor.
|
|
98
|
+
address mockDeployer = address(0x5001);
|
|
99
|
+
vm.mockCall(
|
|
100
|
+
mockDeployer, abi.encodeCall(IJBCCIPSuckerDeployer.ccipRemoteChainId, ()), abi.encode(REMOTE_CHAIN_ID)
|
|
101
|
+
);
|
|
102
|
+
vm.mockCall(
|
|
103
|
+
mockDeployer,
|
|
104
|
+
abi.encodeCall(IJBCCIPSuckerDeployer.ccipRemoteChainSelector, ()),
|
|
105
|
+
abi.encode(REMOTE_CHAIN_SELECTOR)
|
|
106
|
+
);
|
|
107
|
+
vm.mockCall(mockDeployer, abi.encodeCall(IJBCCIPSuckerDeployer.ccipRouter, ()), abi.encode(MOCK_ROUTER));
|
|
108
|
+
|
|
109
|
+
// Deploy singleton harness.
|
|
110
|
+
CCIPSuckerHarness singleton = new CCIPSuckerHarness(
|
|
111
|
+
JBCCIPSuckerDeployer(payable(mockDeployer)),
|
|
112
|
+
IJBDirectory(DIRECTORY),
|
|
113
|
+
IJBTokens(TOKENS),
|
|
114
|
+
IJBPermissions(PERMISSIONS),
|
|
115
|
+
JBAddToBalanceMode.MANUAL,
|
|
116
|
+
FORWARDER
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
// Clone and initialize.
|
|
120
|
+
sucker = CCIPSuckerHarness(payable(LibClone.cloneDeterministic(address(singleton), "ccip_refund_test")));
|
|
121
|
+
sucker.initialize(PROJECT_ID);
|
|
122
|
+
|
|
123
|
+
// Mock directory for ownerOf (needed for mapToken permission checks).
|
|
124
|
+
vm.mockCall(DIRECTORY, abi.encodeCall(IJBDirectory.PROJECTS, ()), abi.encode(PROJECT));
|
|
125
|
+
vm.mockCall(PROJECT, abi.encodeCall(IERC721.ownerOf, (PROJECT_ID)), abi.encode(address(this)));
|
|
126
|
+
|
|
127
|
+
// Put code at MOCK_ROUTER so etch works.
|
|
128
|
+
vm.etch(MOCK_ROUTER, bytes("0x1"));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/// @notice Set up CCIP router mocks for a successful bridge operation.
|
|
132
|
+
function _mockCCIPSuccess(uint256 fee) internal {
|
|
133
|
+
// Mock getFee to return the specified fee.
|
|
134
|
+
vm.mockCall(MOCK_ROUTER, abi.encodeWithSelector(IRouterClient.getFee.selector), abi.encode(fee));
|
|
135
|
+
|
|
136
|
+
// Mock ccipSend to succeed (return a messageId).
|
|
137
|
+
vm.mockCall(
|
|
138
|
+
MOCK_ROUTER, abi.encodeWithSelector(IRouterClient.ccipSend.selector), abi.encode(bytes32(uint256(0xabcdef)))
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/// @notice Set up a token mapping and insert outbox items.
|
|
143
|
+
function _setupOutbox(address token, uint256 amount) internal {
|
|
144
|
+
sucker.test_setRemoteToken(
|
|
145
|
+
token,
|
|
146
|
+
JBRemoteToken({
|
|
147
|
+
enabled: true,
|
|
148
|
+
emergencyHatch: false,
|
|
149
|
+
minGas: 200_000,
|
|
150
|
+
addr: bytes32(uint256(uint160(makeAddr("remoteToken")))),
|
|
151
|
+
minBridgeAmount: 0
|
|
152
|
+
})
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
// Insert a leaf into the outbox.
|
|
156
|
+
sucker.test_insertIntoTree(1 ether, token, amount, bytes32(uint256(uint160(address(this)))));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// =========================================================================
|
|
160
|
+
// M-2: Non-payable caller — refund failure emits event, does NOT revert
|
|
161
|
+
// =========================================================================
|
|
162
|
+
|
|
163
|
+
/// @notice When refund fails (non-payable caller), toRemote should succeed and emit TransportPaymentRefundFailed.
|
|
164
|
+
function test_toRemote_nonPayableCaller_emitsEventOnRefundFailure() public {
|
|
165
|
+
address erc20 = makeAddr("bridgedERC20");
|
|
166
|
+
uint256 bridgeFee = 0.05 ether;
|
|
167
|
+
uint256 transportPayment = 0.1 ether;
|
|
168
|
+
uint256 expectedRefund = transportPayment - bridgeFee;
|
|
169
|
+
|
|
170
|
+
_setupOutbox(erc20, 10 ether);
|
|
171
|
+
_mockCCIPSuccess(bridgeFee);
|
|
172
|
+
|
|
173
|
+
// Give the sucker ERC20 balance for the bridge.
|
|
174
|
+
vm.mockCall(erc20, abi.encodeWithSelector(bytes4(keccak256("approve(address,uint256)"))), abi.encode(true));
|
|
175
|
+
|
|
176
|
+
NonPayableCaller caller = new NonPayableCaller();
|
|
177
|
+
vm.deal(address(caller), transportPayment);
|
|
178
|
+
|
|
179
|
+
// The non-payable caller can't receive the refund.
|
|
180
|
+
// M-2 fix: this should emit TransportPaymentRefundFailed instead of reverting.
|
|
181
|
+
vm.expectEmit(true, false, false, true, address(sucker));
|
|
182
|
+
emit JBCCIPSucker.TransportPaymentRefundFailed(address(caller), expectedRefund);
|
|
183
|
+
|
|
184
|
+
caller.callToRemote{value: transportPayment}(address(sucker), erc20);
|
|
185
|
+
|
|
186
|
+
// The bridge operation completed successfully despite the refund failure.
|
|
187
|
+
// The excess funds remain in the sucker contract.
|
|
188
|
+
assertEq(sucker.test_getOutboxBalance(erc20), 0, "Outbox balance should be cleared");
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/// @notice When refund succeeds (payable caller), no event is emitted.
|
|
192
|
+
function test_toRemote_payableCaller_refundSucceeds() public {
|
|
193
|
+
address erc20 = makeAddr("bridgedERC20");
|
|
194
|
+
uint256 bridgeFee = 0.05 ether;
|
|
195
|
+
uint256 transportPayment = 0.1 ether;
|
|
196
|
+
|
|
197
|
+
_setupOutbox(erc20, 10 ether);
|
|
198
|
+
_mockCCIPSuccess(bridgeFee);
|
|
199
|
+
|
|
200
|
+
vm.mockCall(erc20, abi.encodeWithSelector(bytes4(keccak256("approve(address,uint256)"))), abi.encode(true));
|
|
201
|
+
|
|
202
|
+
PayableCaller caller = new PayableCaller();
|
|
203
|
+
|
|
204
|
+
// Snapshot balance AFTER setup but BEFORE the call.
|
|
205
|
+
uint256 callerBalanceBefore = address(caller).balance;
|
|
206
|
+
|
|
207
|
+
caller.callToRemote{value: transportPayment}(address(sucker), erc20);
|
|
208
|
+
|
|
209
|
+
// Caller should have received the refund (0.05 ether back from the 0.1 sent).
|
|
210
|
+
uint256 expectedRefund = transportPayment - bridgeFee;
|
|
211
|
+
assertEq(address(caller).balance, callerBalanceBefore + expectedRefund, "Caller should receive refund");
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/// @notice When transportPayment == fees exactly, no refund attempt is made (zero refund skip).
|
|
215
|
+
function test_toRemote_exactFee_noRefundAttempt() public {
|
|
216
|
+
address erc20 = makeAddr("bridgedERC20");
|
|
217
|
+
uint256 bridgeFee = 0.05 ether;
|
|
218
|
+
|
|
219
|
+
_setupOutbox(erc20, 10 ether);
|
|
220
|
+
_mockCCIPSuccess(bridgeFee);
|
|
221
|
+
|
|
222
|
+
vm.mockCall(erc20, abi.encodeWithSelector(bytes4(keccak256("approve(address,uint256)"))), abi.encode(true));
|
|
223
|
+
|
|
224
|
+
NonPayableCaller caller = new NonPayableCaller();
|
|
225
|
+
vm.deal(address(caller), bridgeFee);
|
|
226
|
+
|
|
227
|
+
// With exact fee, refundAmount = 0, so no refund is attempted.
|
|
228
|
+
// Even a non-payable caller should succeed without any event.
|
|
229
|
+
caller.callToRemote{value: bridgeFee}(address(sucker), erc20);
|
|
230
|
+
|
|
231
|
+
// No revert, no event — the zero-refund path was taken.
|
|
232
|
+
assertEq(sucker.test_getOutboxBalance(erc20), 0, "Outbox balance should be cleared");
|
|
233
|
+
}
|
|
234
|
+
}
|