Skip to content

Commit ebd4984

Browse files
committed
evm: Safe cast the trimming shift
1 parent 1570241 commit ebd4984

File tree

3 files changed

+38
-7
lines changed

3 files changed

+38
-7
lines changed

evm/src/libraries/RateLimiter.sol

+4-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import "../interfaces/IRateLimiterEvents.sol";
66
import "./TransceiverHelpers.sol";
77
import "./TransceiverStructs.sol";
88
import "../libraries/TrimmedAmount.sol";
9+
import "openzeppelin-contracts/contracts/utils/math/SafeCast.sol";
910

1011
abstract contract RateLimiter is IRateLimiter, IRateLimiterEvents {
1112
using TrimmedAmountLib for TrimmedAmount;
@@ -169,7 +170,9 @@ abstract contract RateLimiter is IRateLimiter, IRateLimiterEvents {
169170
+ (rateLimitParams.limit.getAmount() * timePassed) / rateLimitDuration;
170171

171172
uint256 result = min(calculatedCapacity, rateLimitParams.limit.getAmount());
172-
return packTrimmedAmount(uint64(result), rateLimitParams.currentCapacity.getDecimals());
173+
return packTrimmedAmount(
174+
SafeCast.toUint64(result), rateLimitParams.currentCapacity.getDecimals()
175+
);
173176
}
174177
}
175178

evm/src/libraries/TrimmedAmount.sol

+5-6
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
/// @dev TrimmedAmount is a utility library to handle token amounts with different decimals
33
pragma solidity >=0.8.8 <0.9.0;
44

5+
import "openzeppelin-contracts/contracts/utils/math/SafeCast.sol";
6+
57
/// @dev TrimmedAmount is a bit-packed representation of a token amount and its decimals.
68
/// @dev 64 bits: [0 - 64] amount
79
/// @dev 8 bits: [64 - 72] decimals
@@ -122,7 +124,7 @@ library TrimmedAmountLib {
122124
saturatedSum = saturatedSum > type(uint64).max ? type(uint64).max : saturatedSum;
123125
}
124126

125-
return packTrimmedAmount(uint64(saturatedSum), getDecimals(a));
127+
return packTrimmedAmount(SafeCast.toUint64(saturatedSum), getDecimals(a));
126128
}
127129

128130
/// @dev scale the amount from original decimals to target decimals (base 10)
@@ -145,7 +147,7 @@ library TrimmedAmountLib {
145147
function shift(TrimmedAmount amount, uint8 toDecimals) internal pure returns (TrimmedAmount) {
146148
uint8 actualToDecimals = minUint8(TRIMMED_DECIMALS, toDecimals);
147149
return packTrimmedAmount(
148-
uint64(scale(getAmount(amount), getDecimals(amount), actualToDecimals)),
150+
SafeCast.toUint64(scale(getAmount(amount), getDecimals(amount), actualToDecimals)),
149151
actualToDecimals
150152
);
151153
}
@@ -173,10 +175,7 @@ library TrimmedAmountLib {
173175

174176
// NOTE: amt after trimming must fit into uint64 (that's the point of
175177
// trimming, as Solana only supports uint64 for token amts)
176-
if (amountScaled > type(uint64).max) {
177-
revert AmountTooLarge(amt);
178-
}
179-
return packTrimmedAmount(uint64(amountScaled), actualToDecimals);
178+
return packTrimmedAmount(SafeCast.toUint64(amountScaled), actualToDecimals);
180179
}
181180

182181
function untrim(TrimmedAmount amt, uint8 toDecimals) internal pure returns (uint256) {

evm/test/NttManager.t.sol

+29
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,35 @@ contract TestNttManager is Test, INttManagerEvents, IRateLimiterEvents {
460460
assertEq(s3, 2);
461461
}
462462

463+
function test_transferWithAmountAndDecimalsThatCouldOverflow() public {
464+
// The source chain has 18 decimals trimmed to 8, and the peer has 6 decimals trimmed to 6
465+
nttManager.setPeer(chainId, toWormholeFormat(address(0x1)), 6, type(uint64).max);
466+
467+
address user_A = address(0x123);
468+
address user_B = address(0x456);
469+
DummyToken token = DummyToken(nttManager.token());
470+
uint8 decimals = token.decimals();
471+
assertEq(decimals, 18);
472+
473+
token.mintDummy(address(user_A), type(uint256).max);
474+
475+
vm.startPrank(user_A);
476+
token.approve(address(nttManager), type(uint256).max);
477+
478+
// When transferring to a chain with 6 decimals the amount will get trimmed to 6 decimals
479+
// and then scaled back up to 8 for local accounting. If we get the trimmed amount to be
480+
// type(uint64).max, then when scaling up we could overflow. We safely cast to prevent this.
481+
482+
uint256 amount = type(uint64).max * 10 ** (decimals - 6);
483+
484+
vm.expectRevert("SafeCast: value doesn't fit in 64 bits");
485+
nttManager.transfer(amount, chainId, toWormholeFormat(user_B), false, new bytes(1));
486+
487+
// A (slightly) more sensible amount should work normally
488+
amount = (type(uint64).max * 10 ** (decimals - 6 - 2)) - 150000000000; // Subtract this to make sure we don't have dust
489+
nttManager.transfer(amount, chainId, toWormholeFormat(user_B), false, new bytes(1));
490+
}
491+
463492
function test_attestationQuorum() public {
464493
address user_B = address(0x456);
465494

0 commit comments

Comments
 (0)