Skip to content

Commit 03a8d0d

Browse files
authored
fix: scale input amount (#168)
<!--- Provide a general summary of your changes in the Title above --> In case of different decimals for input and output token, we need to properly scale input amount. Fixes made for across and lifi, similar edge case errors exist on mayan and rhinestone but didn't bother resolving them as they're not used. Please double check could this break something else or are there more cases of this that I might've missed. ## Description <!--- Describe your changes in detail --> ## Related Issue Or Context <!--- If suggesting a new feature or change, please discuss it in an issue first --> <!--- If fixing a bug, there should be an issue describing it with steps to reproduce --> <!--- Otherwise, describe context and motivation for change herre --> Closes: #<issue> ## How Has This Been Tested? Testing details. <!--- Please describe in detail how you tested your changes. --> <!--- Include details of your testing environment, and the tests you ran to --> <!--- see how your change affects other areas of the code, etc. --> ## Types of changes <!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: --> - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation ## Checklist: <!--- Go over all the following points, and put an `x` in all the boxes that apply. --> <!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! --> - [ ] I have commented my code, particularly in hard-to-understand areas. - [ ] I have ensured that all acceptance criteria (or expected behavior) from issue are met - [ ] I have updated the documentation locally and in docs. - [ ] I have added tests to cover my changes. - [ ] I have ensured that all the checks are passing and green, I've signed the CLA bot
1 parent 78a02c7 commit 03a8d0d

6 files changed

Lines changed: 189 additions & 8 deletions

File tree

app/app.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,7 @@ func Run() error {
256256
)
257257
acrossMh := evmMessage.NewAcrossMessageHandler(
258258
*c.GeneralChainConfig.Id,
259+
tokenStore,
259260
acrossPools,
260261
repayerAddresses,
261262
coordinator,

chains/evm/message/across.go

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@ import (
1313
"github.com/libp2p/go-libp2p/core/host"
1414
"github.com/libp2p/go-libp2p/core/peer"
1515
"github.com/rs/zerolog/log"
16+
"github.com/sprintertech/sprinter-signing/chains"
1617
"github.com/sprintertech/sprinter-signing/chains/evm/calls/events"
1718
"github.com/sprintertech/sprinter-signing/chains/evm/signature"
1819
"github.com/sprintertech/sprinter-signing/comm"
20+
"github.com/sprintertech/sprinter-signing/config"
1921
"github.com/sprintertech/sprinter-signing/tss"
2022
"github.com/sprintertech/sprinter-signing/tss/ecdsa/signing"
2123
"github.com/sygmaprotocol/sygma-core/relayer/message"
@@ -55,7 +57,8 @@ type DepositFetcher interface {
5557
}
5658

5759
type AcrossMessageHandler struct {
58-
chainID uint64
60+
chainID uint64
61+
tokenStore config.TokenStore
5962

6063
pools map[uint64]common.Address
6164
repayers map[uint64]common.Address
@@ -72,6 +75,7 @@ type AcrossMessageHandler struct {
7275

7376
func NewAcrossMessageHandler(
7477
chainID uint64,
78+
tokenStore config.TokenStore,
7579
pools map[uint64]common.Address,
7680
repayers map[uint64]common.Address,
7781
coordinator Coordinator,
@@ -84,6 +88,7 @@ func NewAcrossMessageHandler(
8488
) *AcrossMessageHandler {
8589
return &AcrossMessageHandler{
8690
chainID: chainID,
91+
tokenStore: tokenStore,
8792
pools: pools,
8893
repayers: repayers,
8994
coordinator: coordinator,
@@ -123,7 +128,36 @@ func (h *AcrossMessageHandler) HandleMessage(m *message.Message) (*proposal.Prop
123128
return nil, err
124129
}
125130

126-
if data.BorrowAmount.Cmp(d.InputAmount) > 0 {
131+
sourceTokenAddress := common.BytesToAddress(d.InputToken[:])
132+
symbol, srcToken, err := h.tokenStore.ConfigByAddress(h.chainID, sourceTokenAddress)
133+
if err != nil {
134+
err = fmt.Errorf(
135+
"failed to get source token for address %s on chain %d: %w",
136+
sourceTokenAddress.Hex(),
137+
h.chainID,
138+
err,
139+
)
140+
data.ErrChn <- err
141+
return nil, err
142+
}
143+
144+
destToken, err := h.tokenStore.ConfigBySymbol(
145+
d.DestinationChainId.Uint64(),
146+
symbol,
147+
)
148+
if err != nil {
149+
err = fmt.Errorf(
150+
"failed to get destination token by symbol %s on chain %d: %w",
151+
symbol,
152+
d.DestinationChainId.Uint64(),
153+
err,
154+
)
155+
data.ErrChn <- err
156+
return nil, err
157+
}
158+
159+
scaledInputAmount := chains.ScaleTokenAmount(d.InputAmount, int64(srcToken.Decimals), int64(destToken.Decimals))
160+
if data.BorrowAmount.Cmp(scaledInputAmount) > 0 {
127161
err := fmt.Errorf("borrow amount exceeds input amount")
128162
data.ErrChn <- err
129163
return nil, err
@@ -133,7 +167,7 @@ func (h *AcrossMessageHandler) HandleMessage(m *message.Message) (*proposal.Prop
133167
context.Background(),
134168
h.chainID,
135169
data.DepositTxHash,
136-
common.BytesToAddress(d.InputToken[12:]),
170+
sourceTokenAddress,
137171
d.InputAmount)
138172
if err != nil {
139173
data.ErrChn <- err
@@ -151,7 +185,7 @@ func (h *AcrossMessageHandler) HandleMessage(m *message.Message) (*proposal.Prop
151185
unlockHash, err := signature.BorrowUnlockHash(
152186
calldata,
153187
data.BorrowAmount,
154-
common.BytesToAddress(d.OutputToken[12:]),
188+
common.BytesToAddress(d.OutputToken[:]),
155189
d.DestinationChainId,
156190
h.pools[d.DestinationChainId.Uint64()],
157191
data.Deadline,

chains/evm/message/across_test.go

Lines changed: 83 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"github.com/sprintertech/sprinter-signing/comm"
1717
mock_communication "github.com/sprintertech/sprinter-signing/comm/mock"
1818
mock_host "github.com/sprintertech/sprinter-signing/comm/p2p/mock/host"
19+
"github.com/sprintertech/sprinter-signing/config"
1920
"github.com/sprintertech/sprinter-signing/keyshare"
2021
mock_tss "github.com/sprintertech/sprinter-signing/tss/ecdsa/common/mock"
2122
"github.com/stretchr/testify/suite"
@@ -79,12 +80,30 @@ func (s *AcrossMessageHandlerTestSuite) SetupTest() {
7980
// Ethereum: 0x93a9d5e32f5c81cbd17ceb842edc65002e3a79da4efbdc9f1e1f7e97fbcd669b
8081
s.validLog, _ = hex.DecodeString("000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc200000000000000000000000082af49447d8a07e3bd95bd0d56f35241523fbab100000000000000000000000000000000000000000000000000119baee0ab0400000000000000000000000000000000000000000000000000001199073ea3008d0000000000000000000000000000000000000000000000000000000067bc6e3f0000000000000000000000000000000000000000000000000000000067bc927b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000001886a1eb051c10f20c7386576a6a0716b20b2734000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001400000000000000000000000000000000000000000000000000000000000000000")
8182

82-
confirmations := make(map[uint64]uint64)
83-
confirmations[1000] = 100
84-
confirmations[2000] = 200
83+
// Addresses derived the same way the handler does: common.BytesToAddress(token[:])
84+
inputToken6Arr := fillBytes32("input_token_address_1234567890")
85+
outputToken6Arr := fillBytes32("output_token_address_0987654321")
86+
inputToken6Addr := common.BytesToAddress(inputToken6Arr[:])
87+
outputToken6Addr := common.BytesToAddress(outputToken6Arr[:])
88+
inputToken18Addr := common.HexToAddress("0x1111111111111111111111111111111111111111")
89+
outputToken18to6Addr := common.HexToAddress("0x2222222222222222222222222222222222222222")
90+
91+
tokenStore := config.TokenStore{
92+
Tokens: map[uint64]map[string]config.TokenConfig{
93+
1: {
94+
"USDC": {Address: inputToken6Addr, Decimals: 6},
95+
"WUSDC": {Address: inputToken18Addr, Decimals: 18},
96+
},
97+
137: {
98+
"USDC": {Address: outputToken6Addr, Decimals: 6},
99+
"WUSDC": {Address: outputToken18to6Addr, Decimals: 6},
100+
},
101+
},
102+
}
85103

86104
s.handler = message.NewAcrossMessageHandler(
87105
1,
106+
tokenStore,
88107
pools,
89108
repayers,
90109
s.mockCoordinator,
@@ -270,3 +289,64 @@ func (s *AcrossMessageHandlerTestSuite) Test_HandleMessage_ValidDeposit() {
270289
err = <-errChn
271290
s.Nil(err)
272291
}
292+
293+
func (s *AcrossMessageHandlerTestSuite) Test_HandleMessage_BorrowAmountExceedsScaledInputAmount() {
294+
s.mockCommunication.EXPECT().Broadcast(
295+
gomock.Any(),
296+
gomock.Any(),
297+
comm.AcrossMsg,
298+
fmt.Sprintf("%d-%s", 1, comm.AcrossSessionID),
299+
).Return(nil)
300+
p, _ := pstoremem.NewPeerstore()
301+
s.mockHost.EXPECT().Peerstore().Return(p)
302+
303+
// WUSDC on src chain has 18 decimals, on dst chain has 6 decimals.
304+
// InputAmount of 1e18 (1 token at 18 dec) scales to 1e6 (1 token at 6 dec).
305+
// BorrowAmount of 1e6+1 must be rejected.
306+
var inputToken18Arr [32]byte
307+
copy(inputToken18Arr[12:], common.HexToAddress("0x1111111111111111111111111111111111111111").Bytes())
308+
309+
deposit := &events.AcrossDeposit{
310+
InputToken: inputToken18Arr,
311+
OutputToken: fillBytes32("output_token_address_0987654321"),
312+
InputAmount: new(big.Int).Exp(big.NewInt(10), big.NewInt(18), nil), // 1e18
313+
OutputAmount: big.NewInt(990000),
314+
DestinationChainId: big.NewInt(137),
315+
DepositId: big.NewInt(123456789),
316+
//nolint:gosec
317+
QuoteTimestamp: uint32(time.Now().Unix()),
318+
//nolint:gosec
319+
ExclusivityDeadline: uint32(time.Now().Add(10 * time.Minute).Unix()),
320+
//nolint:gosec
321+
FillDeadline: uint32(time.Now().Add(1 * time.Hour).Unix()),
322+
Depositor: fillBytes32("depositor_address_abcdef123456"),
323+
Recipient: fillBytes32("recipient_address_654321fedcba"),
324+
ExclusiveRelayer: fillBytes32("relayer_address_112233445566"),
325+
Message: []byte("Sample message for AcrossDeposit"),
326+
}
327+
s.mockDepositFetcher.EXPECT().Deposit(gomock.Any(), gomock.Any(), gomock.Any()).Return(deposit, nil)
328+
329+
errChn := make(chan error, 1)
330+
ad := &message.AcrossData{
331+
ErrChn: errChn,
332+
DepositId: big.NewInt(2595221),
333+
Nonce: big.NewInt(101),
334+
BorrowAmount: big.NewInt(1_000_001), // 1e6 + 1, exceeds scaled amount of 1e6
335+
LiquidityPool: common.HexToAddress("0xbe526bA5d1ad94cC59D7A79d99A59F607d31A657"),
336+
Caller: common.HexToAddress("0x5ECF7351930e4A251193aA022Ef06249C6cBfa27"),
337+
RepaymentChainID: 10,
338+
}
339+
m := &coreMessage.Message{
340+
Data: ad,
341+
Source: 1,
342+
Destination: 2,
343+
}
344+
345+
prop, err := s.handler.HandleMessage(m)
346+
347+
s.Nil(prop)
348+
s.ErrorContains(err, "borrow amount exceeds input amount")
349+
350+
err = <-errChn
351+
s.ErrorContains(err, "borrow amount exceeds input amount")
352+
}

chains/evm/message/lifiEscrow.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"github.com/libp2p/go-libp2p/core/host"
1313
"github.com/libp2p/go-libp2p/core/peer"
1414
"github.com/rs/zerolog/log"
15+
"github.com/sprintertech/sprinter-signing/chains"
1516
"github.com/sprintertech/sprinter-signing/chains/evm/calls/consts"
1617
"github.com/sprintertech/sprinter-signing/chains/evm/signature"
1718
"github.com/sprintertech/sprinter-signing/comm"
@@ -262,7 +263,17 @@ func (h *LifiEscrowMessageHandler) verifyOrder(order *lifi.LifiOrder, borrowAmou
262263
return fmt.Errorf("orders with multiple outputs not supported")
263264
}
264265

265-
if order.GenericInputs[0].Amount.Cmp(borrowAmount) == -1 {
266+
tokenIn := common.BytesToAddress(order.GenericInputs[0].TokenAddress[:])
267+
symbol, srcToken, err := h.tokenStore.ConfigByAddress(h.chainID, tokenIn)
268+
if err != nil {
269+
return err
270+
}
271+
dstToken, err := h.tokenStore.ConfigBySymbol(order.Order.Outputs[0].ChainID, symbol)
272+
if err != nil {
273+
return err
274+
}
275+
scaledInputAmount := chains.ScaleTokenAmount(order.GenericInputs[0].Amount, int64(srcToken.Decimals), int64(dstToken.Decimals))
276+
if scaledInputAmount.Cmp(borrowAmount) == -1 {
266277
return fmt.Errorf("order input is less than requested borrow amount")
267278
}
268279

chains/util.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,19 @@ func CalculateStartingBlock(startBlock *big.Int, blockConfirmations *big.Int) (*
1717
startBlock.Sub(startBlock, mod)
1818
return startBlock, nil
1919
}
20+
21+
// ScaleTokenAmount scales amount from srcDecimals to dstDecimals.
22+
// Formula: amount / 10^(srcDecimals-dstDecimals)
23+
// When src > dst (e.g. BSC USDC 18 -> Base USDC 6): divides by 10^12.
24+
// When src < dst: exponent is negative, so effectively multiplies.
25+
func ScaleTokenAmount(amount *big.Int, srcDecimals, dstDecimals int64) *big.Int {
26+
if srcDecimals == dstDecimals {
27+
return new(big.Int).Set(amount)
28+
}
29+
if srcDecimals > dstDecimals {
30+
scale := new(big.Int).Exp(big.NewInt(10), big.NewInt(srcDecimals-dstDecimals), nil)
31+
return new(big.Int).Div(amount, scale)
32+
}
33+
scale := new(big.Int).Exp(big.NewInt(10), big.NewInt(dstDecimals-srcDecimals), nil)
34+
return new(big.Int).Mul(amount, scale)
35+
}

chains/util_test.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,42 @@ func (s *UtilTestSuite) Test_CalculateStartingBlock_Nil() {
3535
s.Nil(res)
3636
s.NotNil(err)
3737
}
38+
39+
func (s *UtilTestSuite) TestScaleTokenAmount() {
40+
tests := []struct {
41+
name string
42+
amount *big.Int
43+
srcDecimals int64
44+
dstDecimals int64
45+
want *big.Int
46+
}{
47+
{
48+
name: "same decimals — no scaling",
49+
amount: big.NewInt(1_000_000),
50+
srcDecimals: 6,
51+
dstDecimals: 6,
52+
want: big.NewInt(1_000_000),
53+
},
54+
{
55+
name: "18 to 6 decimals",
56+
amount: big.NewInt(1_000_000_000_000_000_000),
57+
srcDecimals: 18,
58+
dstDecimals: 6,
59+
want: big.NewInt(1_000_000),
60+
},
61+
{
62+
name: "6 to 18 decimals",
63+
amount: big.NewInt(1_000_000),
64+
srcDecimals: 6,
65+
dstDecimals: 18,
66+
want: big.NewInt(1_000_000_000_000_000_000),
67+
},
68+
}
69+
70+
for _, tt := range tests {
71+
s.Run(tt.name, func() {
72+
got := ScaleTokenAmount(tt.amount, tt.srcDecimals, tt.dstDecimals)
73+
s.Equal(tt.want, got)
74+
})
75+
}
76+
}

0 commit comments

Comments
 (0)