@across-protocol/contracts 5.0.11-alpha.4 → 5.0.12-alpha.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.
Files changed (104) hide show
  1. package/contracts/external/interfaces/CCTPInterfaces.sol +18 -0
  2. package/contracts/handlers/MulticallHandler.sol +7 -1
  3. package/contracts/interfaces/SponsoredCCTPInterface.sol +11 -0
  4. package/contracts/libraries/SafeTransferERC20.sol +21 -0
  5. package/contracts/libraries/SponsoredCCTPQuoteLib.sol +22 -0
  6. package/contracts/libraries/TronTransferLib.sol +42 -0
  7. package/contracts/periphery/SpokePoolPeriphery.sol +4 -0
  8. package/contracts/periphery/counterfactual/CounterfactualDepositSpokePool.sol +7 -3
  9. package/contracts/periphery/counterfactual/CounterfactualDepositSpokePoolTr.sol +36 -0
  10. package/contracts/periphery/counterfactual/WithdrawImplementation.sol +3 -6
  11. package/contracts/periphery/counterfactual/WithdrawImplementationTron.sol +22 -0
  12. package/contracts/periphery/mintburn/ArbitraryEVMFlowExecutor.sol +38 -15
  13. package/contracts/periphery/mintburn/sponsored-cctp/SponsoredCCTPDstPeriphery.sol +41 -12
  14. package/contracts/sp1-helios/SP1AutoVerifier.sol +1 -1
  15. package/contracts/spoke-pools/SpokePool.sol +11 -6
  16. package/contracts/spoke-pools/Tron_SpokePool.sol +65 -0
  17. package/contracts/test/MockCCTP.sol +10 -0
  18. package/contracts/test/MockTronUSDT.sol +36 -0
  19. package/contracts/tron/TronCounterfactualImports.sol +4 -3
  20. package/contracts/tron/TronImports.sol +2 -2
  21. package/contracts/tron/TronPeripheryImports.sol +1 -1
  22. package/dist/broadcast/deployed-addresses.json +186 -121
  23. package/dist/evm/artifacts/AcrossMessageHandlerMock.sol/AcrossMessageHandlerMock.json +1 -1
  24. package/dist/evm/artifacts/AdminWithdrawManager.sol/AdminWithdrawManager.json +1 -1
  25. package/dist/evm/artifacts/Arbitrum_Adapter.sol/Arbitrum_Adapter.json +1 -1
  26. package/dist/evm/artifacts/Arbitrum_CustomGasToken_Adapter.sol/Arbitrum_CustomGasToken_Adapter.json +1 -1
  27. package/dist/evm/artifacts/Arbitrum_RescueAdapter.sol/Arbitrum_RescueAdapter.json +1 -1
  28. package/dist/evm/artifacts/Arbitrum_SendTokensAdapter.sol/Arbitrum_SendTokensAdapter.json +1 -1
  29. package/dist/evm/artifacts/Arbitrum_SpokePool.sol/Arbitrum_SpokePool.json +1 -1
  30. package/dist/evm/artifacts/Arbitrum_WithdrawalHelper.sol/Arbitrum_WithdrawalHelper.json +1 -1
  31. package/dist/evm/artifacts/Base_Adapter.sol/Base_Adapter.json +1 -1
  32. package/dist/evm/artifacts/Blast_Adapter.sol/Blast_Adapter.json +1 -1
  33. package/dist/evm/artifacts/Blast_SpokePool.sol/Blast_SpokePool.json +1 -1
  34. package/dist/evm/artifacts/Boba_SpokePool.sol/Boba_SpokePool.json +1 -1
  35. package/dist/evm/artifacts/CCTPInterfaces.sol/ITokenMessengerV2.json +1 -1
  36. package/dist/evm/artifacts/CCTPInterfaces.sol/ITokenMinter.json +1 -1
  37. package/dist/evm/artifacts/Cher_SpokePool.sol/Cher_SpokePool.json +1 -1
  38. package/dist/evm/artifacts/CircleCCTPAdapter.sol/CircleDomainIds.json +1 -1
  39. package/dist/evm/artifacts/CounterfactualDepositCCTP.sol/CounterfactualDepositCCTP.json +1 -1
  40. package/dist/evm/artifacts/CounterfactualDepositSpokePool.sol/CounterfactualDepositSpokePool.json +1 -1
  41. package/dist/evm/artifacts/CounterfactualDepositSpokePoolTr.sol/CounterfactualDepositSpokePoolTr.json +1 -0
  42. package/dist/evm/artifacts/DoctorWho_Adapter.sol/DoctorWho_Adapter.json +1 -1
  43. package/dist/evm/artifacts/DstOFTHandler.sol/DstOFTHandler.json +1 -1
  44. package/dist/evm/artifacts/Ethereum_SpokePool.sol/Ethereum_SpokePool.json +1 -1
  45. package/dist/evm/artifacts/HubPoolTestBase.sol/MockAddressWhitelist.json +1 -1
  46. package/dist/evm/artifacts/HubPoolTestBase.sol/MockFinder.json +1 -1
  47. package/dist/evm/artifacts/HubPoolTestBase.sol/MockIdentifierWhitelist.json +1 -1
  48. package/dist/evm/artifacts/HubPoolTestBase.sol/MockLpTokenFactory.json +1 -1
  49. package/dist/evm/artifacts/HubPoolTestBase.sol/MockOptimisticOracle.json +1 -1
  50. package/dist/evm/artifacts/HubPoolTestBase.sol/MockStore.json +1 -1
  51. package/dist/evm/artifacts/Ink_SpokePool.sol/Ink_SpokePool.json +1 -1
  52. package/dist/evm/artifacts/Lens_SpokePool.sol/Lens_SpokePool.json +1 -1
  53. package/dist/evm/artifacts/Linea_Adapter.sol/Linea_Adapter.json +1 -1
  54. package/dist/evm/artifacts/Linea_SpokePool.sol/Linea_SpokePool.json +1 -1
  55. package/dist/evm/artifacts/Lisk_Adapter.sol/Lisk_Adapter.json +1 -1
  56. package/dist/evm/artifacts/Lisk_SpokePool.sol/Lisk_SpokePool.json +1 -1
  57. package/dist/evm/artifacts/MockBedrockStandardBridge.sol/MockBedrockCrossDomainMessenger.json +1 -1
  58. package/dist/evm/artifacts/MockBedrockStandardBridge.sol/MockBedrockL1StandardBridge.json +1 -1
  59. package/dist/evm/artifacts/MockBedrockStandardBridge.sol/MockBedrockL2StandardBridge.json +1 -1
  60. package/dist/evm/artifacts/MockCCTP.sol/MockCCTPMessageTransmitter.json +1 -1
  61. package/dist/evm/artifacts/MockCCTP.sol/MockCCTPMessenger.json +1 -1
  62. package/dist/evm/artifacts/MockCCTP.sol/MockCCTPMessengerV2.json +1 -1
  63. package/dist/evm/artifacts/MockCCTP.sol/MockCCTPMinter.json +1 -1
  64. package/dist/evm/artifacts/MockOptimism_SpokePool.sol/MockOptimism_SpokePool.json +1 -1
  65. package/dist/evm/artifacts/MockSpokePool.sol/MockSpokePool.json +1 -1
  66. package/dist/evm/artifacts/MockSpokePoolV2.sol/MockSpokePoolV2.json +1 -1
  67. package/dist/evm/artifacts/MockTronUSDT.sol/MockTronUSDT.json +1 -0
  68. package/dist/evm/artifacts/Mode_Adapter.sol/Mode_Adapter.json +1 -1
  69. package/dist/evm/artifacts/MulticallHandler.sol/MulticallHandler.json +1 -1
  70. package/dist/evm/artifacts/OP_Adapter.sol/OP_Adapter.json +1 -1
  71. package/dist/evm/artifacts/OP_SpokePool.sol/OP_SpokePool.json +1 -1
  72. package/dist/evm/artifacts/Optimism_Adapter.sol/Optimism_Adapter.json +1 -1
  73. package/dist/evm/artifacts/Optimism_SpokePool.sol/Optimism_SpokePool.json +1 -1
  74. package/dist/evm/artifacts/Ovm_SpokePool.sol/Ovm_SpokePool.json +1 -1
  75. package/dist/evm/artifacts/Ovm_WithdrawalHelper.sol/Ovm_WithdrawalHelper.json +1 -1
  76. package/dist/evm/artifacts/PermissionedMulticallHandler.sol/PermissionedMulticallHandler.json +1 -1
  77. package/dist/evm/artifacts/PolygonZkEVM_SpokePool.sol/PolygonZkEVM_SpokePool.json +1 -1
  78. package/dist/evm/artifacts/Polygon_Adapter.sol/Polygon_Adapter.json +1 -1
  79. package/dist/evm/artifacts/Polygon_SpokePool.sol/Polygon_SpokePool.json +1 -1
  80. package/dist/evm/artifacts/SP1AutoVerifier.sol/SP1AutoVerifier.json +1 -1
  81. package/dist/evm/artifacts/Scroll_SpokePool.sol/Scroll_SpokePool.json +1 -1
  82. package/dist/evm/artifacts/Solana_Adapter.sol/Solana_Adapter.json +1 -1
  83. package/dist/evm/artifacts/SpokePool.sol/SpokePool.json +1 -1
  84. package/dist/evm/artifacts/SpokePoolPeriphery.sol/SpokePoolPeriphery.json +1 -1
  85. package/dist/evm/artifacts/SpokePoolPeriphery.sol/SwapProxy.json +1 -1
  86. package/dist/evm/artifacts/SponsoredCCTPDstPeriphery.sol/SponsoredCCTPDstPeriphery.json +1 -1
  87. package/dist/evm/artifacts/SponsoredCCTPInterface.sol/SponsoredCCTPInterface.json +1 -1
  88. package/dist/evm/artifacts/SponsoredCCTPSrcPeriphery.sol/SponsoredCCTPSrcPeriphery.json +1 -1
  89. package/dist/evm/artifacts/SponsoredOFTSrcPeriphery.sol/SponsoredOFTSrcPeriphery.json +1 -1
  90. package/dist/evm/artifacts/TronTransferLib.sol/TronTransferLib.json +1 -0
  91. package/dist/evm/artifacts/Tron_SpokePool.sol/Tron_SpokePool.json +1 -0
  92. package/dist/evm/artifacts/Universal_Adapter.sol/Universal_Adapter.json +1 -1
  93. package/dist/evm/artifacts/Universal_SpokePool.sol/Universal_SpokePool.json +1 -1
  94. package/dist/evm/artifacts/WithdrawImplementation.sol/WithdrawImplementation.json +1 -1
  95. package/dist/evm/artifacts/WithdrawImplementationTron.sol/WithdrawImplementationTron.json +1 -0
  96. package/dist/evm/artifacts/WorldChain_SpokePool.sol/WorldChain_SpokePool.json +1 -1
  97. package/dist/evm/artifacts/ZkStack_Adapter.sol/ZkStack_Adapter.json +1 -1
  98. package/dist/evm/artifacts/ZkStack_CustomGasToken_Adapter.sol/ZkStack_CustomGasToken_Adapter.json +1 -1
  99. package/dist/evm/artifacts/ZkSync_SpokePool.sol/ZkSync_SpokePool.json +1 -1
  100. package/dist/evm/artifacts/Zora_Adapter.sol/Zora_Adapter.json +1 -1
  101. package/dist/evm/artifacts/eraVM_EIP7702.sol/SimpleContract.json +1 -1
  102. package/dist/evm/artifacts/eraVM_EIP7702.sol/SpokePoolEIP7702Test.json +1 -1
  103. package/dist/evm/artifacts/eraVM_EIP7702.sol/TestableMockSpokePool.json +1 -1
  104. package/package.json +3 -3
@@ -58,6 +58,14 @@ interface ITokenMessenger {
58
58
  }
59
59
 
60
60
  interface ITokenMessengerV2 {
61
+ /**
62
+ * @notice Minter responsible for minting and burning tokens on the local domain.
63
+ * Used on destination to prove that a received message would mint via the expected
64
+ * TokenMinter, and to resolve the local token that corresponds to a remote burnToken.
65
+ * https://github.com/circlefin/evm-cctp-contracts/blob/63ab1f0ac06ce0793c0bbfbb8d09816bc211386d/src/v2/TokenMessengerV2.sol
66
+ */
67
+ function localMinter() external view returns (ITokenMinter minter);
68
+
61
69
  // Source: https://github.com/circlefin/evm-cctp-contracts/blob/63ab1f0ac06ce0793c0bbfbb8d09816bc211386d/src/v2/TokenMessengerV2.sol#L138C1-L166C15
62
70
  /**
63
71
  * @notice Deposits and burns tokens from sender to be minted on destination domain.
@@ -139,6 +147,16 @@ interface ITokenMinter {
139
147
  * @return burnLimit maximum burn amount per message for token
140
148
  */
141
149
  function burnLimitsPerMessage(address token) external view returns (uint256);
150
+
151
+ /**
152
+ * @notice Returns the local token that is associated with the given remote burnToken on `remoteDomain`.
153
+ * Returns address(0) when no link has been configured.
154
+ * https://github.com/circlefin/evm-cctp-contracts/blob/63ab1f0ac06ce0793c0bbfbb8d09816bc211386d/src/TokenMinter.sol#L155
155
+ * @param remoteDomain CCTP source domain
156
+ * @param remoteToken burnToken (as bytes32) on the remote domain
157
+ * @return localToken the locally-minted token, or address(0) if the pair is not linked
158
+ */
159
+ function getLocalToken(uint32 remoteDomain, bytes32 remoteToken) external view returns (address localToken);
142
160
  }
143
161
 
144
162
  /**
@@ -10,7 +10,13 @@ import "@openzeppelin/contracts-v4/security/ReentrancyGuard.sol";
10
10
  /**
11
11
  * @title Across Multicall contract that allows a user to specify a series of calls that should be made by the handler
12
12
  * via the message field in the deposit.
13
- * @dev This contract makes the calls blindly. The contract will send any remaining tokens The caller should ensure that the tokens received by the handler are completely consumed.
13
+ * @dev This contract makes the calls blindly. The caller should ensure that the tokens received by the handler are
14
+ * completely consumed; otherwise leftover balances will be sent to the fallbackRecipient (when one is provided)
15
+ * or remain on this contract.
16
+ *
17
+ * @dev This contract is a stateless utility with no per-user accounting or admin rescue. Tokens delivered to it
18
+ * are expected to be consumed in the same transaction; any balances left on the contract can be claimed by any
19
+ * caller.
14
20
  */
15
21
  contract MulticallHandler is AcrossMessageHandler, ReentrancyGuard {
16
22
  using SafeERC20 for IERC20;
@@ -24,6 +24,17 @@ interface SponsoredCCTPInterface is SponsoredExecutionModeInterface {
24
24
  // Error thrown when the CCTP message transmitter receive message fails.
25
25
  error CCTPMessageTransmitterFailed();
26
26
 
27
+ // Error thrown when the amount actually minted into this contract by the CCTP call does not match
28
+ // the `amount - feeExecuted` encoded in the message.
29
+ error InvalidMintedAmount();
30
+
31
+ // Error thrown when the CCTP message's top-level `recipient` is not the configured TokenMessenger.
32
+ error InvalidRecipient();
33
+
34
+ // Error thrown when the TokenMinter does not link the message's remote burnToken to this periphery's
35
+ // `baseToken` on the local domain.
36
+ error InvalidMintedToken();
37
+
27
38
  // Error thrown when direct flow destination handler is not a contract.
28
39
  error InvalidDirectHandler();
29
40
 
@@ -0,0 +1,21 @@
1
+ // SPDX-License-Identifier: BUSL-1.1
2
+ pragma solidity ^0.8.0;
3
+
4
+ import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
5
+ import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
6
+
7
+ /**
8
+ * @notice Mixin exposing a virtual `_safeTransfer` hook. Default implementation uses
9
+ * OZ `SafeERC20.safeTransfer`. Inheritors may override to swap in alternative
10
+ * ERC20 transfer semantics.
11
+ */
12
+ abstract contract SafeTransferERC20 {
13
+ // This mixin is the only place in the codebase permitted to call `IERC20.safeTransfer`
14
+ // directly. Inheriting contracts restrict their own `using` directives to exclude
15
+ // `safeTransfer` so all transfer call sites are forced through this overridable hook.
16
+ using { SafeERC20.safeTransfer } for IERC20;
17
+
18
+ function _safeTransfer(address token, address to, uint256 amount) internal virtual {
19
+ IERC20(token).safeTransfer(to, amount);
20
+ }
21
+ }
@@ -89,6 +89,28 @@ library SponsoredCCTPQuoteLib {
89
89
  );
90
90
  }
91
91
 
92
+ /**
93
+ * @notice Reads the top-level CCTP message `recipient` (i.e. the contract MessageTransmitter delivers to,
94
+ * typically the TokenMessenger that performs the mint). Assumes `message` has passed length validation.
95
+ */
96
+ function extractRecipient(bytes memory message) internal pure returns (bytes32) {
97
+ return message.toBytes32(RECIPIENT_INDEX);
98
+ }
99
+
100
+ /**
101
+ * @notice Reads the CCTP source domain from the message header. Assumes `message` has passed length validation.
102
+ */
103
+ function extractSourceDomain(bytes memory message) internal pure returns (uint32) {
104
+ return message.toUint32(SOURCE_DOMAIN_INDEX);
105
+ }
106
+
107
+ /**
108
+ * @notice Reads the burnToken (as bytes32) from the burn message body. Assumes `message` has passed length validation.
109
+ */
110
+ function extractBurnToken(bytes memory message) internal pure returns (bytes32) {
111
+ return message.toBytes32(MESSAGE_BODY_INDEX + BURN_TOKEN_INDEX);
112
+ }
113
+
92
114
  /**
93
115
  * @notice Validates the message that is received from CCTP. If this checks fails, then the quote on source chain was invalid
94
116
  * and we are unable to retrieve user's address to send the funds to. In that case the funds will stay in this contract.
@@ -0,0 +1,42 @@
1
+ // SPDX-License-Identifier: BUSL-1.1
2
+ pragma solidity ^0.8.0;
3
+
4
+ import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
5
+
6
+ /**
7
+ * @notice Balance-delta ERC20 transfer for tokens whose `transfer` returns non-standard
8
+ * values. Specifically targets Tron USDT, which returns false even on success.
9
+ * @dev Two-error model: `_safeTransferBalanceCheck` reverts with `TronTransferCallReverted`
10
+ * if the underlying call reverts, or `TronTransferBalanceMismatch` if the call returns
11
+ * but the recipient's balance does not increase by exactly `amount`. `_balanceDeltaTransfer`
12
+ * is the bool-pair primitive both wrappers (revert / no-revert) share — callers needing a
13
+ * collapsed bool can AND the two flags. Assumes no fee-on-transfer.
14
+ *
15
+ * IERC20 and IERC20Upgradeable produce bytewise-identical calldata for
16
+ * `transfer(address,uint256)` and `balanceOf(address)`, so this library is safe to call
17
+ * from contracts using either OZ variant.
18
+ */
19
+ library TronTransferLib {
20
+ error TronTransferCallReverted();
21
+ error TronTransferBalanceMismatch();
22
+
23
+ /// @dev Returns (callOk, balanceOk). callOk=false means the low-level call reverted;
24
+ /// balanceOk=false means the call returned but balance did not change by exactly `amount`.
25
+ /// When callOk=false, balanceOk is also false (no balance check performed).
26
+ function _balanceDeltaTransfer(
27
+ address token,
28
+ address to,
29
+ uint256 amount
30
+ ) internal returns (bool callOk, bool balanceOk) {
31
+ uint256 pre = IERC20(token).balanceOf(to);
32
+ (callOk, ) = token.call(abi.encodeCall(IERC20.transfer, (to, amount)));
33
+ if (!callOk) return (false, false);
34
+ balanceOk = IERC20(token).balanceOf(to) == pre + amount;
35
+ }
36
+
37
+ function _safeTransferBalanceCheck(address token, address to, uint256 amount) internal {
38
+ (bool callOk, bool balanceOk) = _balanceDeltaTransfer(token, to, amount);
39
+ if (!callOk) revert TronTransferCallReverted();
40
+ if (!balanceOk) revert TronTransferBalanceMismatch();
41
+ }
42
+ }
@@ -21,6 +21,10 @@ import { AddressToBytes32 } from "../libraries/AddressConverters.sol";
21
21
  * @title SwapProxy
22
22
  * @notice A dedicated proxy contract that isolates swap execution to mitigate frontrunning vulnerabilities.
23
23
  * The SpokePoolPeriphery transfers tokens to this contract, which performs the swap and returns tokens back to the periphery.
24
+ *
25
+ * @dev This contract is a stateless utility with no per-user accounting or admin rescue. Tokens delivered to it
26
+ * are expected to be consumed in the same transaction; any balances left on the contract can be claimed by any
27
+ * caller.
24
28
  * @custom:security-contact bugs@across.to
25
29
  */
26
30
  contract SwapProxy is ReentrancyGuard {
@@ -8,6 +8,7 @@ import { EIP712 } from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
8
8
  import { V3SpokePoolInterface } from "../../interfaces/V3SpokePoolInterface.sol";
9
9
  import { ICounterfactualImplementation } from "../../interfaces/ICounterfactualImplementation.sol";
10
10
  import { NATIVE_ASSET, BPS_SCALAR } from "./CounterfactualConstants.sol";
11
+ import { SafeTransferERC20 } from "../../libraries/SafeTransferERC20.sol";
11
12
 
12
13
  /**
13
14
  * @notice Route parameters committed to in the merkle leaf.
@@ -51,8 +52,11 @@ struct SpokePoolSubmitterData {
51
52
  * cannot sign `speedUpV3Deposit` messages.
52
53
  * @custom:security-contact bugs@across.to
53
54
  */
54
- contract CounterfactualDepositSpokePool is ICounterfactualImplementation, EIP712 {
55
- using SafeERC20 for IERC20;
55
+ contract CounterfactualDepositSpokePool is ICounterfactualImplementation, EIP712, SafeTransferERC20 {
56
+ // Restrict the `using` attachment to `forceApprove` only. All `safeTransfer` calls must go
57
+ // through the `_safeTransfer` hook (inherited from `SafeTransferERC20`) so chain-specific
58
+ // variants can override transfer semantics in one place.
59
+ using { SafeERC20.forceApprove } for IERC20;
56
60
 
57
61
  uint256 internal constant EXCHANGE_RATE_SCALAR = 1e18;
58
62
 
@@ -152,7 +156,7 @@ contract CounterfactualDepositSpokePool is ICounterfactualImplementation, EIP712
152
156
  (bool success, ) = sd.executionFeeRecipient.call{ value: dp.executionFee }("");
153
157
  if (!success) revert NativeTransferFailed();
154
158
  } else {
155
- IERC20(inputToken).safeTransfer(sd.executionFeeRecipient, dp.executionFee);
159
+ _safeTransfer(inputToken, sd.executionFeeRecipient, dp.executionFee);
156
160
  }
157
161
  }
158
162
 
@@ -0,0 +1,36 @@
1
+ // SPDX-License-Identifier: BUSL-1.1
2
+ pragma solidity ^0.8.0;
3
+
4
+ import { CounterfactualDepositSpokePool } from "./CounterfactualDepositSpokePool.sol";
5
+ import { TronTransferLib } from "../../libraries/TronTransferLib.sol";
6
+
7
+ /**
8
+ * @title CounterfactualDepositSpokePoolTr
9
+ * @notice Tron-specific variant of `CounterfactualDepositSpokePool` for chains where the
10
+ * input token may be Tron USDT (whose `transfer` returns false on success).
11
+ * @dev Inherits everything from the mainline implementation and overrides the
12
+ * `_safeTransfer` hook to use a balance-delta success check that tolerates
13
+ * Tron USDT's non-standard return value. `forceApprove` is unaffected — `approve`
14
+ * returns true correctly on Tron USDT.
15
+ *
16
+ * The EIP-712 domain name is inherited from the parent (`CounterfactualDepositSpokePool`).
17
+ * Cross-implementation signature replay is already prevented by the `verifyingContract`
18
+ * field of the EIP-712 domain: each clone's address is derived via CREATE2 from its
19
+ * implementation address, so a signature for a mainline clone does not verify against
20
+ * a Tron-variant clone.
21
+ * @custom:security-contact bugs@across.to
22
+ */
23
+ contract CounterfactualDepositSpokePoolTr is CounterfactualDepositSpokePool {
24
+ constructor(
25
+ address _spokePool,
26
+ address _signer,
27
+ address _wrappedNativeToken
28
+ ) CounterfactualDepositSpokePool(_spokePool, _signer, _wrappedNativeToken) {} // solhint-disable-line no-empty-blocks
29
+
30
+ /// @dev TRON OVERRIDE: was `IERC20(token).safeTransfer(to, amount)` in the parent.
31
+ /// `TronTransferLib._safeTransferBalanceCheck` uses a balance-delta success check so it
32
+ /// tolerates Tron USDT's non-standard `transfer` return value.
33
+ function _safeTransfer(address token, address to, uint256 amount) internal override {
34
+ TronTransferLib._safeTransferBalanceCheck(token, to, amount);
35
+ }
36
+ }
@@ -1,10 +1,9 @@
1
1
  // SPDX-License-Identifier: BUSL-1.1
2
2
  pragma solidity ^0.8.0;
3
3
 
4
- import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
5
- import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
6
4
  import { ICounterfactualImplementation } from "../../interfaces/ICounterfactualImplementation.sol";
7
5
  import { NATIVE_ASSET } from "./CounterfactualConstants.sol";
6
+ import { SafeTransferERC20 } from "../../libraries/SafeTransferERC20.sol";
8
7
 
9
8
  /**
10
9
  * @notice Withdrawal parameters committed to in the merkle leaf.
@@ -22,9 +21,7 @@ struct WithdrawParams {
22
21
  * @dev Called via delegatecall from the CounterfactualDeposit dispatcher. `address(this)` is the clone
23
22
  * and `msg.sender` is the original caller.
24
23
  */
25
- contract WithdrawImplementation is ICounterfactualImplementation {
26
- using SafeERC20 for IERC20;
27
-
24
+ contract WithdrawImplementation is ICounterfactualImplementation, SafeTransferERC20 {
28
25
  event Withdraw(address indexed token, address indexed to, uint256 amount);
29
26
 
30
27
  error Unauthorized();
@@ -46,7 +43,7 @@ contract WithdrawImplementation is ICounterfactualImplementation {
46
43
  (bool success, ) = to.call{ value: amount }("");
47
44
  if (!success) revert NativeTransferFailed();
48
45
  } else {
49
- IERC20(token).safeTransfer(to, amount);
46
+ _safeTransfer(token, to, amount);
50
47
  }
51
48
 
52
49
  emit Withdraw(token, to, amount);
@@ -0,0 +1,22 @@
1
+ // SPDX-License-Identifier: BUSL-1.1
2
+ pragma solidity ^0.8.0;
3
+
4
+ import { WithdrawImplementation } from "./WithdrawImplementation.sol";
5
+ import { TronTransferLib } from "../../libraries/TronTransferLib.sol";
6
+
7
+ /**
8
+ * @title WithdrawImplementationTron
9
+ * @notice Tron-specific variant of `WithdrawImplementation`. Inherits from the mainline
10
+ * contract and overrides the `_safeTransfer` hook to use a balance-delta
11
+ * success check that tolerates Tron USDT's non-standard `transfer` return value.
12
+ * Native-asset withdrawals are unchanged.
13
+ * @custom:security-contact bugs@across.to
14
+ */
15
+ contract WithdrawImplementationTron is WithdrawImplementation {
16
+ /// @dev TRON OVERRIDE: was `IERC20(token).safeTransfer(to, amount)` in the parent.
17
+ /// `TronTransferLib._safeTransferBalanceCheck` uses a balance-delta success check so it
18
+ /// tolerates Tron USDT's non-standard `transfer` return value.
19
+ function _safeTransfer(address token, address to, uint256 amount) internal override {
20
+ TronTransferLib._safeTransferBalanceCheck(token, to, amount);
21
+ }
22
+ }
@@ -60,9 +60,17 @@ abstract contract ArbitraryEVMFlowExecutor {
60
60
  // Decode the compressed action data
61
61
  CompressedCall[] memory compressedCalls = abi.decode(params.actionData, (CompressedCall[]));
62
62
 
63
- // Snapshot balances
64
- uint256 initialAmountSnapshot = IERC20(params.initialToken).balanceOf(address(this));
65
- uint256 finalAmountSnapshot = IERC20(params.commonParams.finalToken).balanceOf(address(this));
63
+ // Sweep any pre-existing dust on MulticallHandler so it cannot pollute our balance snapshots
64
+ _drainMulticallHandlerDust(params.initialToken);
65
+ if (params.commonParams.finalToken != params.initialToken) {
66
+ _drainMulticallHandlerDust(params.commonParams.finalToken);
67
+ }
68
+
69
+ bool differentTokens = params.initialToken != params.commonParams.finalToken;
70
+
71
+ // Read "starting balance initial token"(sBI) and "starting balance final token"(sBF)
72
+ uint256 sBI = IERC20(params.initialToken).balanceOf(address(this));
73
+ uint256 sBF = differentTokens ? IERC20(params.commonParams.finalToken).balanceOf(address(this)) : sBI;
66
74
 
67
75
  // Transfer tokens to MulticallHandler
68
76
  IERC20(params.initialToken).safeTransfer(multicallHandler, params.commonParams.amountInEVM);
@@ -82,19 +90,21 @@ abstract contract ArbitraryEVMFlowExecutor {
82
90
  instructions
83
91
  );
84
92
 
85
- uint256 finalAmount;
86
- // This means the swap (if one was intended) didn't happen (action failed), so we use the initial token as the final token.
87
- if (initialAmountSnapshot == IERC20(params.initialToken).balanceOf(address(this))) {
88
- params.commonParams.finalToken = params.initialToken;
89
- finalAmount = params.commonParams.amountInEVM;
90
- } else {
91
- uint256 finalBalance = IERC20(params.commonParams.finalToken).balanceOf(address(this));
92
- if (finalBalance >= finalAmountSnapshot) {
93
- // This means the swap did happen, so we check the balance of the output token and send it.
94
- finalAmount = finalBalance - finalAmountSnapshot;
93
+ // Default to initial-token accounting; overwrite below if finalToken was actually produced.
94
+ // Ending balance initial token
95
+ uint256 eBI = IERC20(params.initialToken).balanceOf(address(this));
96
+ uint256 finalAmount = params.commonParams.amountInEVM + eBI - sBI;
97
+ if (differentTokens) {
98
+ // Ending balance final token
99
+ uint256 eBF = IERC20(params.commonParams.finalToken).balanceOf(address(this));
100
+
101
+ // Any positive finalToken delta is treated as successful execution when initialToken != finalToken. If any
102
+ // (or all) of the initialToken amount is unspent during the execution, it lands back into this contract and
103
+ // can be withdrawn by the admin
104
+ if (eBF > sBF) {
105
+ finalAmount = eBF - sBF;
95
106
  } else {
96
- // If we somehow lost final tokens (e.g. by depositing into some contract), just set the finalAmount to 0.
97
- finalAmount = 0;
107
+ params.commonParams.finalToken = params.initialToken;
98
108
  }
99
109
  }
100
110
 
@@ -155,6 +165,19 @@ abstract contract ArbitraryEVMFlowExecutor {
155
165
  return abi.encode(instructions);
156
166
  }
157
167
 
168
+ /// @notice Drains any pre-existing balance of token from MulticallHandler
169
+ function _drainMulticallHandlerDust(address token) internal {
170
+ if (IERC20(token).balanceOf(multicallHandler) == 0) return;
171
+
172
+ // Empty instructions with `fallbackRecipient` set. Causes MulticallHandler to call `_drainRemainingTokens`, which
173
+ // does a safeTransfer of `token` to `fallbackRecipient`, this contract, essentially removing dust
174
+ bytes memory instructions = abi.encode(
175
+ MulticallHandler.Instructions({ calls: new MulticallHandler.Call[](0), fallbackRecipient: address(this) })
176
+ );
177
+
178
+ MulticallHandler(payable(multicallHandler)).handleV3AcrossMessage(token, 0, address(this), instructions);
179
+ }
180
+
158
181
  /// @notice Calculates proportional fees to sponsor in finalToken, given the fees to sponsor in initial token and initial amount
159
182
  function _calcExtraFeesFinal(
160
183
  uint256 amount,
@@ -2,7 +2,7 @@
2
2
  pragma solidity ^0.8.0;
3
3
 
4
4
  import { BaseModuleHandler } from "../BaseModuleHandler.sol";
5
- import { IMessageTransmitterV2 } from "../../../external/interfaces/CCTPInterfaces.sol";
5
+ import { IMessageTransmitterV2, ITokenMessengerV2 } from "../../../external/interfaces/CCTPInterfaces.sol";
6
6
  import { SponsoredCCTPQuoteLib } from "../../../libraries/SponsoredCCTPQuoteLib.sol";
7
7
  import { SponsoredCCTPInterface } from "../../../interfaces/SponsoredCCTPInterface.sol";
8
8
  import { Bytes32ToAddress } from "../../../libraries/AddressConverters.sol";
@@ -27,6 +27,10 @@ contract SponsoredCCTPDstPeriphery is BaseModuleHandler, SponsoredCCTPInterface,
27
27
  /// @notice The CCTP message transmitter contract.
28
28
  IMessageTransmitterV2 public immutable cctpMessageTransmitter;
29
29
 
30
+ /// @notice The CCTP token messenger contract that is the expected top-level recipient of MessageTransmitter
31
+ /// deliveries and owns the TokenMinter used to resolve burnToken -> local token for non-direct-deposit flows.
32
+ ITokenMessengerV2 public immutable cctpTokenMessenger;
33
+
30
34
  /// @notice Base token associated with this handler. The one we receive from the CCTP bridge
31
35
  address public immutable baseToken;
32
36
 
@@ -52,6 +56,8 @@ contract SponsoredCCTPDstPeriphery is BaseModuleHandler, SponsoredCCTPInterface,
52
56
  /**
53
57
  * @notice Constructor for the SponsoredCCTPDstPeriphery contract.
54
58
  * @param _cctpMessageTransmitter The address of the CCTP message transmitter contract.
59
+ * @param _cctpTokenMessenger The address of the CCTP token messenger contract. Used to pin that a received
60
+ * message actually routed through the real mint flow and that the remote burnToken links to `baseToken`.
55
61
  * @param _signer The address of the signer that was used to sign the quotes.
56
62
  * @param _donationBox The address of the donation box contract. This is used to store funds that are used for sponsored flows.
57
63
  * @param _baseToken The address of the base token which would be the USDC on HyperEVM.
@@ -59,6 +65,7 @@ contract SponsoredCCTPDstPeriphery is BaseModuleHandler, SponsoredCCTPInterface,
59
65
  */
60
66
  constructor(
61
67
  address _cctpMessageTransmitter,
68
+ address _cctpTokenMessenger,
62
69
  address _signer,
63
70
  address _donationBox,
64
71
  address _baseToken,
@@ -67,6 +74,7 @@ contract SponsoredCCTPDstPeriphery is BaseModuleHandler, SponsoredCCTPInterface,
67
74
  baseToken = _baseToken;
68
75
 
69
76
  cctpMessageTransmitter = IMessageTransmitterV2(_cctpMessageTransmitter);
77
+ cctpTokenMessenger = ITokenMessengerV2(_cctpTokenMessenger);
70
78
 
71
79
  MainStorage storage $ = _getMainStorage();
72
80
  $.signer = _signer;
@@ -127,10 +135,15 @@ contract SponsoredCCTPDstPeriphery is BaseModuleHandler, SponsoredCCTPInterface,
127
135
  bytes memory message,
128
136
  bytes memory attestation
129
137
  ) external nonReentrant onlyRole(PERMISSIONED_BOT_ROLE) {
138
+ uint256 baseBalBefore = IERC20Metadata(baseToken).balanceOf(address(this));
130
139
  bool success = cctpMessageTransmitter.receiveMessage(message, attestation);
131
140
  if (!success) {
132
141
  return;
133
142
  }
143
+ // Forward only what the CCTP call actually credited to this contract, so a message that mints nothing
144
+ // (wrong recipient, unlinked burnToken, etc.) cannot drain this contract's prior `baseToken` balance.
145
+ uint256 baseBalAfter = IERC20Metadata(baseToken).balanceOf(address(this));
146
+ uint256 mintedAmount = baseBalAfter - baseBalBefore;
134
147
 
135
148
  // Use try-catch to handle potential abi.decode reverts gracefully
136
149
  try this.validateMessage(message) returns (bool isValid) {
@@ -141,19 +154,14 @@ contract SponsoredCCTPDstPeriphery is BaseModuleHandler, SponsoredCCTPInterface,
141
154
  // Malformed message that causes abi.decode to revert then early return
142
155
  return;
143
156
  }
144
- (SponsoredCCTPInterface.SponsoredCCTPQuote memory quote, uint256 feeExecuted) = SponsoredCCTPQuoteLib
145
- .getSponsoredCCTPQuoteData(message);
146
-
147
- _getMainStorage().usedNonces[quote.nonce] = true;
157
+ (SponsoredCCTPInterface.SponsoredCCTPQuote memory quote, ) = SponsoredCCTPQuoteLib.getSponsoredCCTPQuoteData(
158
+ message
159
+ );
148
160
 
149
- IERC20Metadata(baseToken).safeTransfer(quote.finalRecipient.toAddress(), quote.amount - feeExecuted);
161
+ address finalRecipient = quote.finalRecipient.toAddress();
162
+ IERC20Metadata(baseToken).safeTransfer(finalRecipient, mintedAmount);
150
163
 
151
- emit EmergencyReceiveMessage(
152
- quote.nonce,
153
- quote.finalRecipient.toAddress(),
154
- baseToken,
155
- quote.amount - feeExecuted
156
- );
164
+ emit EmergencyReceiveMessage(quote.nonce, finalRecipient, baseToken, mintedAmount);
157
165
  }
158
166
 
159
167
  /**
@@ -168,6 +176,20 @@ contract SponsoredCCTPDstPeriphery is BaseModuleHandler, SponsoredCCTPInterface,
168
176
  bytes memory attestation,
169
177
  bytes memory signature
170
178
  ) external nonReentrant authorizeFundedFlow {
179
+ // Check that the CCTP message was actually routed through our TokenMessenger (which owns the mint flow),
180
+ // and that its TokenMinter links the remote burnToken to our `baseToken` on this domain.
181
+ if (SponsoredCCTPQuoteLib.extractRecipient(message).toAddressUnchecked() != address(cctpTokenMessenger)) {
182
+ revert InvalidRecipient();
183
+ }
184
+ address localToken = cctpTokenMessenger.localMinter().getLocalToken(
185
+ SponsoredCCTPQuoteLib.extractSourceDomain(message),
186
+ SponsoredCCTPQuoteLib.extractBurnToken(message)
187
+ );
188
+ if (localToken != baseToken) {
189
+ revert InvalidMintedToken();
190
+ }
191
+
192
+ uint256 baseBalBefore = IERC20Metadata(baseToken).balanceOf(address(this));
171
193
  bool success = cctpMessageTransmitter.receiveMessage(message, attestation);
172
194
  if (!success) {
173
195
  revert CCTPMessageTransmitterFailed();
@@ -188,6 +210,13 @@ contract SponsoredCCTPDstPeriphery is BaseModuleHandler, SponsoredCCTPInterface,
188
210
  (SponsoredCCTPInterface.SponsoredCCTPQuote memory quote, uint256 feeExecuted) = SponsoredCCTPQuoteLib
189
211
  .getSponsoredCCTPQuoteData(message);
190
212
 
213
+ // Confirm the CCTP call actually minted `baseToken` to this contract.
214
+ uint256 baseBalAfter = IERC20Metadata(baseToken).balanceOf(address(this));
215
+ uint256 amountAfterFees = quote.amount - feeExecuted;
216
+ if (baseBalAfter - baseBalBefore != amountAfterFees) {
217
+ revert InvalidMintedAmount();
218
+ }
219
+
191
220
  // Validate the quote and the signature. Revert on invalid to prevent griefing attacks
192
221
  // where an attacker provides correct message/attestation but invalid signature.
193
222
  _validateQuoteOrRevert(quote, signature);
@@ -4,7 +4,7 @@ pragma solidity ^0.8.25;
4
4
  import { ISP1Verifier } from "@sp1-contracts/src/ISP1Verifier.sol";
5
5
 
6
6
  /// @title SP1 Auto Verifier
7
- /// @notice A no-op verifier that accepts any proof. Useful for testing SP1Helios without real proofs.
7
+ /// @notice A no-op verifier that accepts any proof.
8
8
  contract SP1AutoVerifier is ISP1Verifier {
9
9
  // pure is intentionally stricter than the interface's view; Solidity allows this and it's correct for a no-op.
10
10
  function verifyProof(bytes32, bytes calldata, bytes calldata) external pure {}
@@ -11,6 +11,7 @@ import "../upgradeable/MultiCallerUpgradeable.sol";
11
11
  import "../upgradeable/EIP712CrossChainUpgradeable.sol";
12
12
  import "../upgradeable/AddressLibUpgradeable.sol";
13
13
  import "../libraries/AddressConverters.sol";
14
+ import { SafeTransferERC20 } from "../libraries/SafeTransferERC20.sol";
14
15
  import { IOFT, SendParam, MessagingFee } from "../interfaces/IOFT.sol";
15
16
  import { OFTTransportAdapter } from "../libraries/OFTTransportAdapter.sol";
16
17
 
@@ -39,9 +40,13 @@ abstract contract SpokePool is
39
40
  MultiCallerUpgradeable,
40
41
  EIP712CrossChainUpgradeable,
41
42
  IDestinationSettler,
42
- OFTTransportAdapter
43
+ OFTTransportAdapter,
44
+ SafeTransferERC20
43
45
  {
44
- using SafeERC20Upgradeable for IERC20Upgradeable;
46
+ // Restrict the `using` attachment to `safeTransferFrom` only. All `safeTransfer` calls must go
47
+ // through the `_safeTransfer` hook (inherited from `SafeTransferERC20`) so chain-specific
48
+ // variants can override transfer semantics in one place.
49
+ using { SafeERC20Upgradeable.safeTransferFrom } for IERC20Upgradeable;
45
50
  using AddressLibUpgradeable for address;
46
51
  using Bytes32ToAddress for bytes32;
47
52
  using AddressToBytes32 for address;
@@ -1241,7 +1246,7 @@ abstract contract SpokePool is
1241
1246
  uint256 refund = relayerRefund[l2TokenAddress.toAddress()][msg.sender];
1242
1247
  if (refund == 0) revert NoRelayerRefundToClaim();
1243
1248
  relayerRefund[l2TokenAddress.toAddress()][msg.sender] = 0;
1244
- IERC20Upgradeable(l2TokenAddress.toAddress()).safeTransfer(refundAddress.toAddress(), refund);
1249
+ _safeTransfer(l2TokenAddress.toAddress(), refundAddress.toAddress(), refund);
1245
1250
 
1246
1251
  emit ClaimedRelayerRefund(l2TokenAddress, refundAddress, refund, msg.sender);
1247
1252
  }
@@ -1429,7 +1434,7 @@ abstract contract SpokePool is
1429
1434
  // Re-implementation of OZ _callOptionalReturnBool to use private logic. Function executes a transfer and returns a
1430
1435
  // bool indicating if the external call was successful, rather than reverting. Original method:
1431
1436
  // https://github.com/OpenZeppelin/openzeppelin-contracts/blob/28aed34dc5e025e61ea0390c18cac875bfde1a78/contracts/token/ERC20/utils/SafeERC20.sol#L188
1432
- function _noRevertTransfer(address token, address to, uint256 amount) internal returns (bool) {
1437
+ function _noRevertTransfer(address token, address to, uint256 amount) internal virtual returns (bool) {
1433
1438
  bool success;
1434
1439
  uint256 returnSize;
1435
1440
  uint256 returnValue;
@@ -1548,7 +1553,7 @@ abstract contract SpokePool is
1548
1553
  wrappedNativeToken.withdraw(amount);
1549
1554
  AddressLibUpgradeable.sendValue(to, amount);
1550
1555
  } else {
1551
- IERC20Upgradeable(address(wrappedNativeToken)).safeTransfer(to, amount);
1556
+ _safeTransfer(address(wrappedNativeToken), to, amount);
1552
1557
  }
1553
1558
  }
1554
1559
 
@@ -1662,7 +1667,7 @@ abstract contract SpokePool is
1662
1667
  } else {
1663
1668
  // Note: Similar to note above, send token directly from the contract to the user in the slow relay case.
1664
1669
  if (!isSlowFill) IERC20Upgradeable(outputToken).safeTransferFrom(msg.sender, recipientToSend, amountToSend);
1665
- else IERC20Upgradeable(outputToken).safeTransfer(recipientToSend, amountToSend);
1670
+ else _safeTransfer(outputToken, recipientToSend, amountToSend);
1666
1671
  }
1667
1672
 
1668
1673
  bytes memory updatedMessage = relayExecution.updatedMessage;
@@ -0,0 +1,65 @@
1
+ // SPDX-License-Identifier: BUSL-1.1
2
+ pragma solidity ^0.8.0;
3
+
4
+ import { IERC20 } from "@openzeppelin/contracts-v4/token/ERC20/IERC20.sol";
5
+
6
+ import { Universal_SpokePool } from "./Universal_SpokePool.sol";
7
+ import { ITokenMessenger } from "../external/interfaces/CCTPInterfaces.sol";
8
+ import { TronTransferLib } from "../libraries/TronTransferLib.sol";
9
+
10
+ /**
11
+ * @notice Tron-specific SpokePool variant that handles non-standard ERC20 implementations.
12
+ * @dev Tron USDT's `transfer` always returns false on success, which breaks the return-value
13
+ * checks in `SafeERC20.safeTransfer` and `SpokePool._noRevertTransfer`. This variant
14
+ * overrides both base hooks (`_noRevertTransfer` and `_safeTransfer`) to delegate to
15
+ * `TronTransferLib`, which performs a balance-delta success check. `transferFrom` is
16
+ * correct on Tron USDT, so paths using `safeTransferFrom` are unchanged.
17
+ *
18
+ * Assumes Tether's `basisPointsRate` fee-on-transfer mechanism stays at zero. If it
19
+ * is ever activated, balance-delta will report failure on successful transfers and
20
+ * USDT routes on this contract will wedge until disabled operationally.
21
+ * @custom:security-contact bugs@across.to
22
+ */
23
+ contract Tron_SpokePool is Universal_SpokePool {
24
+ /// @custom:oz-upgrades-unsafe-allow constructor
25
+ constructor(
26
+ uint256 _adminUpdateBufferSeconds,
27
+ address _helios,
28
+ address _hubPoolStore,
29
+ address _wrappedNativeTokenAddress,
30
+ uint32 _depositQuoteTimeBuffer,
31
+ uint32 _fillDeadlineBuffer,
32
+ IERC20 _l2Usdc,
33
+ ITokenMessenger _cctpTokenMessenger,
34
+ uint32 _oftDstEid,
35
+ uint256 _oftFeeCap
36
+ )
37
+ Universal_SpokePool(
38
+ _adminUpdateBufferSeconds,
39
+ _helios,
40
+ _hubPoolStore,
41
+ _wrappedNativeTokenAddress,
42
+ _depositQuoteTimeBuffer,
43
+ _fillDeadlineBuffer,
44
+ _l2Usdc,
45
+ _cctpTokenMessenger,
46
+ _oftDstEid,
47
+ _oftFeeCap
48
+ )
49
+ {} // solhint-disable-line no-empty-blocks
50
+
51
+ /// @dev Replaces base implementation's return-value-based success detection with a
52
+ /// balance-delta check. Required because Tron USDT's `transfer` returns false
53
+ /// even on successful transfers.
54
+ function _noRevertTransfer(address token, address to, uint256 amount) internal override returns (bool) {
55
+ (bool callOk, bool balanceOk) = TronTransferLib._balanceDeltaTransfer(token, to, amount);
56
+ return callOk && balanceOk;
57
+ }
58
+
59
+ /// @dev Revert-on-failure variant; reverts with `TronTransferCallReverted` or
60
+ /// `TronTransferBalanceMismatch` so callers can distinguish failure modes.
61
+ /// Replaces the base `safeTransfer` call sites (claimRelayerRefund, slow-fill ERC20 path).
62
+ function _safeTransfer(address token, address to, uint256 amount) internal override {
63
+ TronTransferLib._safeTransferBalanceCheck(token, to, amount);
64
+ }
65
+ }