Skip to content

Commit 06e12cd

Browse files
committed
almost there
Signed-off-by: Ihor Farion <ihor@umaproject.org>
1 parent 5c9cccb commit 06e12cd

File tree

4 files changed

+97
-65
lines changed

4 files changed

+97
-65
lines changed

contracts/external/libraries/SharedDecimalsLib.sol

Lines changed: 63 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -3,68 +3,88 @@ pragma solidity ^0.8.0;
33

44
/**
55
* @title SharedDecimalsLib
6-
* @notice Library for handling shared decimals conversions for OFT bridging.
7-
* @dev Logic adapted from LayerZero's OFTCore.sol
6+
* @notice Copied from LZ implementation here:
7+
* `https://github.com/LayerZero-Labs/devtools/blob/16daaee36fe802d11aa99b89c29bb74447354483/packages/oft-evm/contracts/OFTCore.sol#L364`
8+
* Code was not modified beyond removing unrelated OFT/OApp concerns.
89
*/
9-
library SharedDecimalsLib {
10+
abstract contract SharedDecimalsLib {
1011
error InvalidLocalDecimals();
1112
error AmountSDOverflowed(uint256 amountSD);
1213

14+
// @notice Provides a conversion rate when swapping between denominations of SD and LD
15+
// - shareDecimals == SD == shared Decimals
16+
// - localDecimals == LD == local decimals
17+
// @dev Considers that tokens have different decimal amounts on various chains.
18+
// @dev eg.
19+
// For a token
20+
// - locally with 4 decimals --> 1.2345 => uint(12345)
21+
// - remotely with 2 decimals --> 1.23 => uint(123)
22+
// - The conversion rate would be 10 ** (4 - 2) = 100
23+
// @dev If you want to send 1.2345 -> (uint 12345), you CANNOT represent that value on the remote,
24+
// you can only display 1.23 -> uint(123).
25+
// @dev To preserve the dust that would otherwise be lost on that conversion,
26+
// we need to unify a denomination that can be represented on ALL chains inside of the OFT mesh
27+
uint256 public immutable decimalConversionRate;
28+
1329
/**
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.
30+
* @dev Constructor.
31+
* @param _localDecimals The decimals of the token on the local chain (this chain).
32+
* @param _endpoint Unused (kept for parity with OFTCore signature).
33+
* @param _delegate Unused (kept for parity with OFTCore signature).
34+
*/
35+
constructor(uint8 _localDecimals, address _endpoint, address _delegate) {
36+
_endpoint;
37+
_delegate;
38+
if (_localDecimals < sharedDecimals()) revert InvalidLocalDecimals();
39+
decimalConversionRate = 10 ** (_localDecimals - sharedDecimals());
40+
}
41+
42+
/**
43+
* @dev Retrieves the shared decimals of the OFT.
44+
* @return The shared decimals of the OFT.
45+
*
46+
* @dev Sets an implicit cap on the amount of tokens, over uint64.max() will need some sort of outbound cap / totalSupply cap
47+
* Lowest common decimal denominator between chains.
48+
* Defaults to 6 decimal places to provide up to 18,446,744,073,709.551615 units (max uint64).
49+
* For tokens exceeding this totalSupply(), they will need to override the sharedDecimals function with something smaller.
50+
* ie. 4 sharedDecimals would be 1,844,674,407,370,955.1615
51+
*/
52+
function sharedDecimals() public view virtual returns (uint8) {
53+
return 6;
54+
}
55+
56+
/**
57+
* @dev Internal function to remove dust from the given local decimal amount.
1658
* @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.
59+
* @return amountLD The amount after removing dust.
2060
*
21-
* @dev Reverts if the _amountLD in shared decimals overflows uint64.
61+
* @dev Prevents the loss of dust when moving amounts between chains with different decimals.
62+
* @dev eg. uint(123) with a conversion rate of 100 becomes uint(100).
2263
*/
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);
64+
function _removeDust(uint256 _amountLD) internal view virtual returns (uint256 amountLD) {
65+
return (_amountLD / decimalConversionRate) * decimalConversionRate;
3366
}
3467

3568
/**
36-
* @notice Convert an amount from shared decimals into local decimals.
3769
* @dev Internal function to convert an amount from shared decimals into local decimals.
3870
* @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.
4171
* @return amountLD The amount in local decimals.
4272
*/
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;
73+
function _toLD(uint64 _amountSD) internal view virtual returns (uint256 amountLD) {
74+
return _amountSD * decimalConversionRate;
5175
}
5276

5377
/**
54-
* @notice Remove dust from the given local decimal amount.
55-
* @dev Internal function to remove dust from the given local decimal amount.
78+
* @dev Internal function to convert an amount from local decimals into shared decimals.
5679
* @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.
80+
* @return amountSD The amount in shared decimals.
81+
*
82+
* @dev Reverts if the _amountLD in shared decimals overflows uint64.
83+
* @dev eg. uint(2**64 + 123) with a conversion rate of 1 wraps around 2**64 to uint(123).
6084
*/
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;
85+
function _toSD(uint256 _amountLD) internal view virtual returns (uint64 amountSD) {
86+
uint256 _amountSD = _amountLD / decimalConversionRate;
87+
if (_amountSD > type(uint64).max) revert AmountSDOverflowed(_amountSD);
88+
return uint64(_amountSD);
6989
}
7090
}

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

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import { SafeERC20 } from "@openzeppelin/contracts-v4/token/ERC20/utils/SafeERC2
2323
* @dev IMPORTANT. `BaseModuleHandler` should always be the first contract in inheritance chain. Read
2424
`BaseModuleHandler` contract code to learn more.
2525
*/
26-
contract DstOFTHandler is BaseModuleHandler, ILayerZeroComposer, ArbitraryEVMFlowExecutor {
26+
contract DstOFTHandler is BaseModuleHandler, SharedDecimalsLib, ILayerZeroComposer, ArbitraryEVMFlowExecutor {
2727
using ComposeMsgCodec for bytes;
2828
using Bytes32ToAddress for bytes32;
2929
using AddressToBytes32 for address;
@@ -84,7 +84,11 @@ contract DstOFTHandler is BaseModuleHandler, ILayerZeroComposer, ArbitraryEVMFlo
8484
address _donationBox,
8585
address _baseToken,
8686
address _multicallHandler
87-
) BaseModuleHandler(_donationBox, _baseToken, DEFAULT_ADMIN_ROLE) ArbitraryEVMFlowExecutor(_multicallHandler) {
87+
)
88+
BaseModuleHandler(_donationBox, _baseToken, DEFAULT_ADMIN_ROLE)
89+
ArbitraryEVMFlowExecutor(_multicallHandler)
90+
SharedDecimalsLib(IERC20Metadata(_baseToken).decimals(), address(0), address(0))
91+
{
8892
baseToken = _baseToken;
8993

9094
if (_baseToken != IOFT(_ioft).token()) {
@@ -162,14 +166,10 @@ contract DstOFTHandler is BaseModuleHandler, ILayerZeroComposer, ArbitraryEVMFlo
162166
// 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)
163167
uint256 amountLD = OFTComposeMsgCodec.amountLD(_message);
164168

165-
// We trust src periphery to encode the correct amount.
166-
// We decode amountSD and convert it to local decimals to compare against amountLD
169+
// We trust src periphery to encode the correct amountSD. It pulls amountLD from the user, using IOFT's sharedDecimals()
170+
// for further conversion to SD. The DEFAULT_ADMIN is responsible for keeping a correct mapping of authorized src peripheries.
167171
uint256 amountSentSD = composeMsg._getAmountSD();
168-
uint256 amountSentLD = SharedDecimalsLib.toLD(
169-
uint64(amountSentSD),
170-
IERC20Metadata(baseToken).decimals(),
171-
IOFT(IOFT_ADDRESS).sharedDecimals()
172-
);
172+
uint256 amountSentLD = _toLD(uint64(amountSentSD));
173173

174174
uint256 extraFeesIncurred = 0;
175175
if (amountSentLD > amountLD) {

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

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { SafeERC20 } from "@openzeppelin/contracts-v4/token/ERC20/utils/SafeERC2
1717

1818
/// @notice Source chain periphery contract for users to interact with to start a sponsored or a non-sponsored flow
1919
/// that allows custom Accross-supported flows on destination chain. Uses LayzerZero's OFT as an underlying bridge
20-
contract SponsoredOFTSrcPeriphery is Ownable {
20+
contract SponsoredOFTSrcPeriphery is Ownable, SharedDecimalsLib {
2121
using AddressToBytes32 for address;
2222
using MinimalLZOptions for bytes;
2323
using SafeERC20 for IERC20;
@@ -87,8 +87,15 @@ contract SponsoredOFTSrcPeriphery is Ownable {
8787
error InsufficientNativeFee();
8888
/// @notice Thrown when array lengths do not match
8989
error ArrayLengthMismatch();
90-
91-
constructor(address _token, address _oftMessenger, uint32 _srcEid, address _signer) {
90+
/// @notice Thrown when maxOftFeeBps is greater than 10000
91+
error InvalidMaxOftFeeBps();
92+
93+
constructor(
94+
address _token,
95+
address _oftMessenger,
96+
uint32 _srcEid,
97+
address _signer
98+
) SharedDecimalsLib(IERC20Metadata(_token).decimals(), address(0), address(0)) {
9299
TOKEN = _token;
93100
OFT_MESSENGER = _oftMessenger;
94101
SRC_EID = _srcEid;
@@ -165,14 +172,11 @@ contract SponsoredOFTSrcPeriphery is Ownable {
165172
function _buildOftTransfer(
166173
Quote calldata quote
167174
) internal view returns (SendParam memory, MessagingFee memory, address) {
168-
uint8 localDecimals = IERC20Metadata(TOKEN).decimals();
169-
uint8 sharedDecimals = IOFT(OFT_MESSENGER).sharedDecimals();
170-
171-
uint256 amountSD = SharedDecimalsLib.toSD(quote.signedParams.amountLD, localDecimals, sharedDecimals);
175+
uint64 amountSD = _toSD(quote.signedParams.amountLD);
172176

173177
bytes memory composeMsg = ComposeMsgCodec._encode(
174178
quote.signedParams.nonce,
175-
amountSD,
179+
uint256(amountSD),
176180
quote.signedParams.deadline,
177181
quote.signedParams.maxBpsToSponsor,
178182
quote.unsignedParams.maxUserSlippageBps,
@@ -189,8 +193,12 @@ contract SponsoredOFTSrcPeriphery is Ownable {
189193
.addExecutorLzReceiveOption(uint128(quote.signedParams.lzReceiveGasLimit), uint128(0))
190194
.addExecutorLzComposeOption(uint16(0), uint128(quote.signedParams.lzComposeGasLimit), uint128(0));
191195

192-
// Use removeDust to calculate minAmountLD in local decimals (on src)
193-
uint256 minAmountLD = SharedDecimalsLib.removeDust(quote.signedParams.amountLD, localDecimals, sharedDecimals);
196+
// Apply maxOftFeeBps in src-local decimals.
197+
// If maxOftFeeBps == 0, this preserves strict behavior: OFT will revert if dust removal reduces amount below min.
198+
if (quote.signedParams.maxOftFeeBps > 10_000) {
199+
revert InvalidMaxOftFeeBps();
200+
}
201+
uint256 minAmountLD = (quote.signedParams.amountLD * (10_000 - quote.signedParams.maxOftFeeBps)) / 10_000;
194202

195203
SendParam memory sendParam = SendParam(
196204
quote.signedParams.dstEid,

script/mintburn/oft/CreateSponsoredDeposit.s.sol

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ 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";
1312
import { IOFT, SendParam, MessagingFee, IOAppCore } from "../../../contracts/interfaces/IOFT.sol";
1413
import { HyperCoreLib } from "../../../contracts/libraries/HyperCoreLib.sol";
1514
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
@@ -277,7 +276,11 @@ contract CreateSponsoredDeposit is Script, Config {
277276
uint8 localDecimals = IERC20Metadata(token).decimals();
278277
uint8 sharedDecimals = IOFT(oftMessenger).sharedDecimals();
279278

280-
uint256 amountSD = SharedDecimalsLib.toSD(quote.signedParams.amountLD, localDecimals, sharedDecimals);
279+
require(localDecimals >= sharedDecimals, "InvalidLocalDecimals");
280+
uint256 decimalConversionRate = 10 ** (localDecimals - sharedDecimals);
281+
uint256 _amountSD = quote.signedParams.amountLD / decimalConversionRate;
282+
require(_amountSD <= type(uint64).max, "AmountSDOverflowed");
283+
uint256 amountSD = uint256(uint64(_amountSD));
281284

282285
bytes memory composeMsg = ComposeMsgCodec._encode(
283286
quote.signedParams.nonce,
@@ -298,7 +301,8 @@ contract CreateSponsoredDeposit is Script, Config {
298301
.addExecutorLzReceiveOption(uint128(quote.signedParams.lzReceiveGasLimit), uint128(0))
299302
.addExecutorLzComposeOption(uint16(0), uint128(quote.signedParams.lzComposeGasLimit), uint128(0));
300303

301-
uint256 minAmountLD = SharedDecimalsLib.removeDust(quote.signedParams.amountLD, localDecimals, sharedDecimals);
304+
require(quote.signedParams.maxOftFeeBps <= 10_000, "maxOftFeeBps > 10000");
305+
uint256 minAmountLD = (quote.signedParams.amountLD * (10_000 - quote.signedParams.maxOftFeeBps)) / 10_000;
302306

303307
SendParam memory sendParam = SendParam({
304308
dstEid: quote.signedParams.dstEid,

0 commit comments

Comments
 (0)