Skip to content

Commit 5c9cccb

Browse files
committed
interim better
Signed-off-by: Ihor Farion <ihor@umaproject.org>
1 parent 820b0f3 commit 5c9cccb

File tree

9 files changed

+125
-58
lines changed

9 files changed

+125
-58
lines changed
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.0;
3+
4+
/**
5+
* @title SharedDecimalsLib
6+
* @notice Library for handling shared decimals conversions for OFT bridging.
7+
* @dev Logic adapted from LayerZero's OFTCore.sol
8+
*/
9+
library SharedDecimalsLib {
10+
error InvalidLocalDecimals();
11+
error AmountSDOverflowed(uint256 amountSD);
12+
13+
/**
14+
* @notice Convert an amount from local decimals into shared decimals.
15+
* @dev Internal function to convert an amount from local decimals into shared decimals.
16+
* @param _amountLD The amount in local decimals.
17+
* @param _localDecimals The decimals of the token on the local chain.
18+
* @param _sharedDecimals The shared decimals of the OFT.
19+
* @return amountSD The amount in shared decimals.
20+
*
21+
* @dev Reverts if the _amountLD in shared decimals overflows uint64.
22+
*/
23+
function toSD(
24+
uint256 _amountLD,
25+
uint8 _localDecimals,
26+
uint8 _sharedDecimals
27+
) internal pure returns (uint64 amountSD) {
28+
if (_localDecimals < _sharedDecimals) revert InvalidLocalDecimals();
29+
uint256 conversionRate = 10 ** (_localDecimals - _sharedDecimals);
30+
uint256 _amountSD = _amountLD / conversionRate;
31+
if (_amountSD > type(uint64).max) revert AmountSDOverflowed(_amountSD);
32+
return uint64(_amountSD);
33+
}
34+
35+
/**
36+
* @notice Convert an amount from shared decimals into local decimals.
37+
* @dev Internal function to convert an amount from shared decimals into local decimals.
38+
* @param _amountSD The amount in shared decimals.
39+
* @param _localDecimals The decimals of the token on the local chain.
40+
* @param _sharedDecimals The shared decimals of the OFT.
41+
* @return amountLD The amount in local decimals.
42+
*/
43+
function toLD(
44+
uint64 _amountSD,
45+
uint8 _localDecimals,
46+
uint8 _sharedDecimals
47+
) internal pure returns (uint256 amountLD) {
48+
if (_localDecimals < _sharedDecimals) revert InvalidLocalDecimals();
49+
uint256 conversionRate = 10 ** (_localDecimals - _sharedDecimals);
50+
return uint256(_amountSD) * conversionRate;
51+
}
52+
53+
/**
54+
* @notice Remove dust from the given local decimal amount.
55+
* @dev Internal function to remove dust from the given local decimal amount.
56+
* @param _amountLD The amount in local decimals.
57+
* @param _localDecimals The decimals of the token on the local chain.
58+
* @param _sharedDecimals The shared decimals of the OFT.
59+
* @return amountLD The amount after removing dust.
60+
*/
61+
function removeDust(
62+
uint256 _amountLD,
63+
uint8 _localDecimals,
64+
uint8 _sharedDecimals
65+
) internal pure returns (uint256 amountLD) {
66+
if (_localDecimals < _sharedDecimals) revert InvalidLocalDecimals();
67+
uint256 conversionRate = 10 ** (_localDecimals - _sharedDecimals);
68+
return (_amountLD / conversionRate) * conversionRate;
69+
}
70+
}

contracts/interfaces/IOFT.sol

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,12 @@ interface IOFT {
5454
*/
5555
function token() external view returns (address);
5656

57+
/**
58+
* @notice Retrieves the shared decimals of the OFT.
59+
* @return sharedDecimals The shared decimals of the OFT.
60+
*/
61+
function sharedDecimals() external view returns (uint8);
62+
5763
/**
5864
* @notice Provides a quote for the send() operation.
5965
* @param _sendParam The parameters for the send() operation.

contracts/periphery/mintburn/sponsored-oft/ComposeMsgCodec.sol

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { BytesLib } from "../../../external/libraries/BytesLib.sol";
55
/// @notice Codec for params passed in OFT `composeMsg`.
66
library ComposeMsgCodec {
77
uint256 internal constant NONCE_OFFSET = 0;
8-
uint256 internal constant AMOUNT_LD_OFFSET = 32;
8+
uint256 internal constant AMOUNT_SD_OFFSET = 32;
99
uint256 internal constant DEADLINE_OFFSET = 64;
1010
uint256 internal constant MAX_BPS_TO_SPONSOR_OFFSET = 96;
1111
uint256 internal constant MAX_USER_SLIPPAGE_BPS_OFFSET = 128;
@@ -19,7 +19,7 @@ library ComposeMsgCodec {
1919

2020
function _encode(
2121
bytes32 nonce,
22-
uint256 amountLD,
22+
uint256 amountSD,
2323
uint256 deadline,
2424
uint256 maxBpsToSponsor,
2525
uint256 maxUserSlippageBps,
@@ -33,7 +33,7 @@ library ComposeMsgCodec {
3333
return
3434
abi.encode(
3535
nonce,
36-
amountLD,
36+
amountSD,
3737
deadline,
3838
maxBpsToSponsor,
3939
maxUserSlippageBps,
@@ -50,8 +50,8 @@ library ComposeMsgCodec {
5050
return BytesLib.toBytes32(data, NONCE_OFFSET);
5151
}
5252

53-
function _getAmountLD(bytes memory data) internal pure returns (uint256 v) {
54-
return BytesLib.toUint256(data, AMOUNT_LD_OFFSET);
53+
function _getAmountSD(bytes memory data) internal pure returns (uint256 v) {
54+
return BytesLib.toUint256(data, AMOUNT_SD_OFFSET);
5555
}
5656

5757
function _getDeadline(bytes memory data) internal pure returns (uint256 v) {

contracts/periphery/mintburn/sponsored-oft/DstOFTHandler.sol

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@ import { IOFT, IOAppCore } from "../../../interfaces/IOFT.sol";
1111
import { HyperCoreFlowExecutor } from "../HyperCoreFlowExecutor.sol";
1212
import { ArbitraryEVMFlowExecutor } from "../ArbitraryEVMFlowExecutor.sol";
1313
import { CommonFlowParams, EVMFlowParams, AccountCreationMode } from "../Structs.sol";
14+
import { SharedDecimalsLib } from "../../../external/libraries/SharedDecimalsLib.sol";
1415

1516
import { IERC20 } from "@openzeppelin/contracts-v4/token/ERC20/IERC20.sol";
17+
import { IERC20Metadata } from "@openzeppelin/contracts-v4/token/ERC20/extensions/IERC20Metadata.sol";
1618
import { SafeERC20 } from "@openzeppelin/contracts-v4/token/ERC20/utils/SafeERC20.sol";
1719

1820
/**
@@ -160,9 +162,14 @@ contract DstOFTHandler is BaseModuleHandler, ILayerZeroComposer, ArbitraryEVMFlo
160162
// Amount received from `lzReceive` on destination. May be different from the amount the user originally sent (if the OFT takes a fee in the bridged token)
161163
uint256 amountLD = OFTComposeMsgCodec.amountLD(_message);
162164

163-
// We trust src periphery to encode the correct amount. This is safe because src periphery pulls the amount it
164-
// encodes here from the user. Decimal conversion is done on src side as well, so no coversions are needed here
165-
uint256 amountSentLD = composeMsg._getAmountLD();
165+
// We trust src periphery to encode the correct amount.
166+
// We decode amountSD and convert it to local decimals to compare against amountLD
167+
uint256 amountSentSD = composeMsg._getAmountSD();
168+
uint256 amountSentLD = SharedDecimalsLib.toLD(
169+
uint64(amountSentSD),
170+
IERC20Metadata(baseToken).decimals(),
171+
IOFT(IOFT_ADDRESS).sharedDecimals()
172+
);
166173

167174
uint256 extraFeesIncurred = 0;
168175
if (amountSentLD > amountLD) {

contracts/periphery/mintburn/sponsored-oft/SponsoredOFTSrcPeriphery.sol

Lines changed: 9 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@ import { ComposeMsgCodec } from "./ComposeMsgCodec.sol";
88
import { IOFT, IOAppCore, SendParam, MessagingFee } from "../../../interfaces/IOFT.sol";
99
import { AddressToBytes32 } from "../../../libraries/AddressConverters.sol";
1010
import { MinimalLZOptions } from "../../../external/libraries/MinimalLZOptions.sol";
11+
import { SharedDecimalsLib } from "../../../external/libraries/SharedDecimalsLib.sol";
1112

1213
import { Ownable } from "@openzeppelin/contracts-v4/access/Ownable.sol";
1314
import { IERC20 } from "@openzeppelin/contracts-v4/token/ERC20/IERC20.sol";
15+
import { IERC20Metadata } from "@openzeppelin/contracts-v4/token/ERC20/extensions/IERC20Metadata.sol";
1416
import { SafeERC20 } from "@openzeppelin/contracts-v4/token/ERC20/utils/SafeERC20.sol";
1517

1618
/// @notice Source chain periphery contract for users to interact with to start a sponsored or a non-sponsored flow
@@ -30,9 +32,6 @@ contract SponsoredOFTSrcPeriphery is Ownable {
3032
/// @notice Source endpoint id
3133
uint32 public immutable SRC_EID;
3234

33-
/// @notice Trusted mapping of dst decimal difference with src decimals for TOKEN
34-
mapping(uint32 dstEid => int8 decimalDiff) public dstEidToDecimalsDiff;
35-
3635
/// @custom:storage-location erc7201:SponsoredOFTSrcPeriphery.main
3736
struct MainStorage {
3837
/// @notice Signer public key to check the signed quote against
@@ -166,14 +165,14 @@ contract SponsoredOFTSrcPeriphery is Ownable {
166165
function _buildOftTransfer(
167166
Quote calldata quote
168167
) internal view returns (SendParam memory, MessagingFee memory, address) {
169-
uint256 amountDstLD = _applyDecimalDiff(
170-
quote.signedParams.amountLD,
171-
dstEidToDecimalsDiff[quote.signedParams.dstEid]
172-
);
168+
uint8 localDecimals = IERC20Metadata(TOKEN).decimals();
169+
uint8 sharedDecimals = IOFT(OFT_MESSENGER).sharedDecimals();
170+
171+
uint256 amountSD = SharedDecimalsLib.toSD(quote.signedParams.amountLD, localDecimals, sharedDecimals);
173172

174173
bytes memory composeMsg = ComposeMsgCodec._encode(
175174
quote.signedParams.nonce,
176-
amountDstLD,
175+
amountSD,
177176
quote.signedParams.deadline,
178177
quote.signedParams.maxBpsToSponsor,
179178
quote.unsignedParams.maxUserSlippageBps,
@@ -190,8 +189,8 @@ contract SponsoredOFTSrcPeriphery is Ownable {
190189
.addExecutorLzReceiveOption(uint128(quote.signedParams.lzReceiveGasLimit), uint128(0))
191190
.addExecutorLzComposeOption(uint16(0), uint128(quote.signedParams.lzComposeGasLimit), uint128(0));
192191

193-
// Use maxOftFeeBps to calculate minAmountLD based on expected destination amount
194-
uint256 minAmountLD = (amountDstLD * (10000 - quote.signedParams.maxOftFeeBps)) / 10000;
192+
// Use removeDust to calculate minAmountLD in local decimals (on src)
193+
uint256 minAmountLD = SharedDecimalsLib.removeDust(quote.signedParams.amountLD, localDecimals, sharedDecimals);
195194

196195
SendParam memory sendParam = SendParam(
197196
quote.signedParams.dstEid,
@@ -209,21 +208,6 @@ contract SponsoredOFTSrcPeriphery is Ownable {
209208
return (sendParam, fee, quote.unsignedParams.refundRecipient);
210209
}
211210

212-
/**
213-
* @notice Applies decimal difference to the amount
214-
* @param amount The amount to adjust
215-
* @param diff The decimal difference (positive: multiply, negative: divide)
216-
* @return The adjusted amount
217-
*/
218-
function _applyDecimalDiff(uint256 amount, int8 diff) internal pure returns (uint256) {
219-
if (diff > 0) {
220-
return amount * (10 ** uint8(diff));
221-
} else if (diff < 0) {
222-
return amount / (10 ** uint8(-diff));
223-
}
224-
return amount;
225-
}
226-
227211
function _validateQuote(Quote calldata quote, bytes calldata signature) internal view {
228212
MainStorage storage $ = _getMainStorage();
229213
if (!QuoteSignLib.isSignatureValid($.signer, quote.signedParams, signature)) {
@@ -243,18 +227,4 @@ contract SponsoredOFTSrcPeriphery is Ownable {
243227
function setSigner(address _newSigner) external onlyOwner {
244228
_getMainStorage().signer = _newSigner;
245229
}
246-
247-
/**
248-
* @notice Sets the decimal difference for destination chains
249-
* @param dstEids Array of destination endpoint IDs
250-
* @param decimalDiffs Array of decimal differences (positive for Src < Dst, negative for Src > Dst)
251-
*/
252-
function setDstEidToDecimalsDiff(uint32[] calldata dstEids, int8[] calldata decimalDiffs) external onlyOwner {
253-
if (dstEids.length != decimalDiffs.length) {
254-
revert ArrayLengthMismatch();
255-
}
256-
for (uint256 i = 0; i < dstEids.length; ++i) {
257-
dstEidToDecimalsDiff[dstEids[i]] = decimalDiffs[i];
258-
}
259-
}
260230
}

contracts/test/MockOFTMessenger.sol

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ contract MockOFTMessenger is IOFT, IOAppCore {
4040
return endpoint_;
4141
}
4242

43+
function sharedDecimals() external pure returns (uint8) {
44+
return 6;
45+
}
46+
4347
// IOFT
4448
function quoteSend(
4549
SendParam calldata /*_sendParam*/,

script/mintburn/oft/CreateSponsoredDeposit.s.sol

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@ import { Quote, SignedQuoteParams, UnsignedQuoteParams } from "../../../contract
99
import { AddressToBytes32 } from "../../../contracts/libraries/AddressConverters.sol";
1010
import { ComposeMsgCodec } from "../../../contracts/periphery/mintburn/sponsored-oft/ComposeMsgCodec.sol";
1111
import { MinimalLZOptions } from "../../../contracts/external/libraries/MinimalLZOptions.sol";
12+
import { SharedDecimalsLib } from "../../../contracts/external/libraries/SharedDecimalsLib.sol";
1213
import { IOFT, SendParam, MessagingFee, IOAppCore } from "../../../contracts/interfaces/IOFT.sol";
1314
import { HyperCoreLib } from "../../../contracts/libraries/HyperCoreLib.sol";
1415
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
16+
import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
1517
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
1618

1719
/// @notice Used in place of // import { QuoteSignLib } from "../contracts/periphery/mintburn/sponsored-oft/QuoteSignLib.sol";
@@ -270,10 +272,16 @@ contract CreateSponsoredDeposit is Script, Config {
270272
Quote memory quote
271273
) internal view returns (MessagingFee memory) {
272274
address oftMessenger = srcPeripheryContract.OFT_MESSENGER();
275+
address token = srcPeripheryContract.TOKEN();
276+
277+
uint8 localDecimals = IERC20Metadata(token).decimals();
278+
uint8 sharedDecimals = IOFT(oftMessenger).sharedDecimals();
279+
280+
uint256 amountSD = SharedDecimalsLib.toSD(quote.signedParams.amountLD, localDecimals, sharedDecimals);
273281

274282
bytes memory composeMsg = ComposeMsgCodec._encode(
275283
quote.signedParams.nonce,
276-
quote.signedParams.amountLD,
284+
amountSD,
277285
quote.signedParams.deadline,
278286
quote.signedParams.maxBpsToSponsor,
279287
quote.unsignedParams.maxUserSlippageBps,
@@ -290,11 +298,13 @@ contract CreateSponsoredDeposit is Script, Config {
290298
.addExecutorLzReceiveOption(uint128(quote.signedParams.lzReceiveGasLimit), uint128(0))
291299
.addExecutorLzComposeOption(uint16(0), uint128(quote.signedParams.lzComposeGasLimit), uint128(0));
292300

301+
uint256 minAmountLD = SharedDecimalsLib.removeDust(quote.signedParams.amountLD, localDecimals, sharedDecimals);
302+
293303
SendParam memory sendParam = SendParam({
294304
dstEid: quote.signedParams.dstEid,
295305
to: quote.signedParams.destinationHandler,
296306
amountLD: quote.signedParams.amountLD,
297-
minAmountLD: quote.signedParams.amountLD,
307+
minAmountLD: minAmountLD,
298308
extraOptions: extraOptions,
299309
composeMsg: composeMsg,
300310
oftCmd: srcPeripheryContract.EMPTY_OFT_COMMAND()

test/evm/foundry/local/ComposeMsgCodec.t.sol

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { ComposeMsgCodec } from "contracts/periphery/mintburn/sponsored-oft/Comp
77
contract ComposeMsgCodecTest is Test {
88
function test_EncodeDecode() public {
99
bytes32 nonce = keccak256("nonce");
10-
uint256 amountLD = 100 ether;
10+
uint256 amountSD = 100 ether;
1111
uint256 deadline = 1234567890;
1212
uint256 maxBpsToSponsor = 500;
1313
uint256 maxUserSlippageBps = 100;
@@ -20,7 +20,7 @@ contract ComposeMsgCodecTest is Test {
2020

2121
bytes memory encoded = ComposeMsgCodec._encode(
2222
nonce,
23-
amountLD,
23+
amountSD,
2424
deadline,
2525
maxBpsToSponsor,
2626
maxUserSlippageBps,
@@ -33,7 +33,7 @@ contract ComposeMsgCodecTest is Test {
3333
);
3434

3535
assertEq(ComposeMsgCodec._getNonce(encoded), nonce, "Nonce mismatch");
36-
assertEq(ComposeMsgCodec._getAmountLD(encoded), amountLD, "AmountLD mismatch");
36+
assertEq(ComposeMsgCodec._getAmountSD(encoded), amountSD, "AmountSD mismatch");
3737
assertEq(ComposeMsgCodec._getDeadline(encoded), deadline, "Deadline mismatch");
3838
assertEq(ComposeMsgCodec._getMaxBpsToSponsor(encoded), maxBpsToSponsor, "MaxBpsToSponsor mismatch");
3939
assertEq(ComposeMsgCodec._getMaxUserSlippageBps(encoded), maxUserSlippageBps, "MaxUserSlippageBps mismatch");
@@ -48,7 +48,7 @@ contract ComposeMsgCodecTest is Test {
4848

4949
function testFuzz_EncodeDecode(
5050
bytes32 nonce,
51-
uint256 amountLD,
51+
uint256 amountSD,
5252
uint256 deadline,
5353
uint256 maxBpsToSponsor,
5454
uint256 maxUserSlippageBps,
@@ -61,7 +61,7 @@ contract ComposeMsgCodecTest is Test {
6161
) public {
6262
bytes memory encoded = ComposeMsgCodec._encode(
6363
nonce,
64-
amountLD,
64+
amountSD,
6565
deadline,
6666
maxBpsToSponsor,
6767
maxUserSlippageBps,
@@ -74,7 +74,7 @@ contract ComposeMsgCodecTest is Test {
7474
);
7575

7676
assertEq(ComposeMsgCodec._getNonce(encoded), nonce, "Nonce mismatch");
77-
assertEq(ComposeMsgCodec._getAmountLD(encoded), amountLD, "AmountLD mismatch");
77+
assertEq(ComposeMsgCodec._getAmountSD(encoded), amountSD, "AmountSD mismatch");
7878
assertEq(ComposeMsgCodec._getDeadline(encoded), deadline, "Deadline mismatch");
7979
assertEq(ComposeMsgCodec._getMaxBpsToSponsor(encoded), maxBpsToSponsor, "MaxBpsToSponsor mismatch");
8080
assertEq(ComposeMsgCodec._getMaxUserSlippageBps(encoded), maxUserSlippageBps, "MaxUserSlippageBps mismatch");

test/evm/foundry/local/SponsoredOFTSrcPeriphery.t.sol

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -148,13 +148,13 @@ contract SponsoredOFTSrcPeripheryTest is Test {
148148
assertEq(spDstEid, quote.signedParams.dstEid, "dstEid mismatch");
149149
assertEq(spTo, quote.signedParams.destinationHandler, "destination handler mismatch");
150150
assertEq(spAmountLD, SEND_AMOUNT, "amountLD mismatch");
151-
assertEq(spMinAmountLD, SEND_AMOUNT, "minAmountLD should equal amountLD (no fee-in-token)");
151+
assertEq(spMinAmountLD, SEND_AMOUNT, "minAmountLD should be SEND_AMOUNT (no dust loss)");
152152
assertEq(spOftCmd.length, 0, "oftCmd must be empty");
153153

154154
// Validate composeMsg encoding (layout from ComposeMsgCodec._encode)
155155
(
156156
bytes32 gotNonce,
157-
uint256 gotAmountLD,
157+
uint256 gotAmountSD,
158158
uint256 gotDeadline,
159159
uint256 gotMaxBpsToSponsor,
160160
uint256 gotMaxUserSlippageBps,
@@ -170,7 +170,7 @@ contract SponsoredOFTSrcPeripheryTest is Test {
170170
);
171171

172172
assertEq(gotNonce, nonce, "nonce mismatch");
173-
assertEq(gotAmountLD, SEND_AMOUNT, "amountLD mismatch");
173+
assertEq(gotAmountSD, SEND_AMOUNT / 1e12, "amountSD mismatch");
174174
assertEq(gotDeadline, deadline, "deadline mismatch");
175175
assertEq(gotMaxBpsToSponsor, 500, "maxBpsToSponsor mismatch");
176176
assertEq(gotMaxUserSlippageBps, 300, "maxUserSlippageBps mismatch");

0 commit comments

Comments
 (0)