Canto - sces60107's results

A layer-1 EVM powered by free public infrastructure.

General Information

Platform: Code4rena

Start Date: 20/06/2023

Pot Size: $36,500 USDC

Total HM: 2

Participants: 23

Period: 3 days

Judge: 0xean

Id: 252

League: ETH

Canto

Findings Distribution

Researcher Performance

Rank: 1/23

Findings: 2

Award: $5,421.24

🌟 Selected for report: 1

πŸš€ Solo Findings: 0

Findings Information

🌟 Selected for report: sces60107

Also found by: 3docSec, Franfran, Lirios, Team_FliBit, dontonka

Labels

bug
3 (High Risk)
primary issue
satisfactory
selected for report
upgraded by judge
H-01

Awards

4404.7619 USDC - $4,404.76

External Links

Lines of code

https://github.com/code-423n4/2023-06-canto/blob/main/Canto/x/coinswap/keeper/swap.go#L212 https://github.com/code-423n4/2023-06-canto/blob/main/Canto/x/coinswap/types/params.go#L34

Vulnerability details

Impact

In the spec, the pre-defined limit of ETH is 0.01 ETHs. But the actual limit in the code is not 0.01 ETH which could result in misleading.

Proof of Concept

In the spec, it said that the pre-defined limit of ETH is 0.01 ETHs https://github.com/code-423n4/2023-06-canto/blob/main/README.md#swap

For risk management purposes, a swap will fail if the input coin amount exceeds a pre-defined limit (10 USDC, 10 USDT, 0.01 ETH) or if the swap amount limit is not defined.

But in x/coinswap/types/params.go, the actual limit of ETH is 1*10e17 which is 0.1 ETH

// Parameter store keys
var (
	KeyFee                    = []byte("Fee")                    // fee key
	KeyPoolCreationFee        = []byte("PoolCreationFee")        // fee key
	KeyTaxRate                = []byte("TaxRate")                // fee key
	KeyStandardDenom          = []byte("StandardDenom")          // standard token denom key
	KeyMaxStandardCoinPerPool = []byte("MaxStandardCoinPerPool") // max standard coin amount per pool
	KeyMaxSwapAmount          = []byte("MaxSwapAmount")          // whitelisted denoms

	DefaultFee                    = sdk.NewDecWithPrec(0, 0)
	DefaultPoolCreationFee        = sdk.NewInt64Coin(sdk.DefaultBondDenom, 0)
	DefaultTaxRate                = sdk.NewDecWithPrec(0, 0)
	DefaultMaxStandardCoinPerPool = sdk.NewIntWithDecimal(10000, 18)
	DefaultMaxSwapAmount          = sdk.NewCoins(
		sdk.NewCoin(UsdcIBCDenom, sdk.NewIntWithDecimal(10, 6)),
		sdk.NewCoin(UsdtIBCDenom, sdk.NewIntWithDecimal(10, 6)),
		sdk.NewCoin(EthIBCDenom, sdk.NewIntWithDecimal(1, 17)),
	)
)

The limit is used in swap.GetMaximumSwapAmount. Wrong could harm the risk management. https://github.com/code-423n4/2023-06-canto/blob/main/Canto/x/coinswap/keeper/swap.go#L212

func (k Keeper) GetMaximumSwapAmount(ctx sdk.Context, denom string) (sdk.Coin, error) {
	params := k.GetParams(ctx)
	for _, coin := range params.MaxSwapAmount {
		if coin.Denom == denom {
			return coin, nil
		}
	}
	return sdk.Coin{}, sdkerrors.Wrap(types.ErrInvalidDenom, fmt.Sprintf("invalid denom: %s, denom is not whitelisted", denom))
}

Tools Used

Manual Review

0.01 ETH should be sdk.NewIntWithDecimal(1, 16)

Assessed type

Error

#0 - c4-pre-sort

2023-06-24T12:15:47Z

JeffCX marked the issue as duplicate of #8

#1 - c4-judge

2023-07-03T20:49:51Z

0xean marked the issue as satisfactory

#2 - c4-judge

2023-07-03T20:50:21Z

0xean changed the severity to 3 (High Risk)

#3 - c4-judge

2023-07-03T21:08:26Z

0xean marked the issue as selected for report

Findings Information

🌟 Selected for report: yaarduck

Also found by: Rolezn, erebus, hihen, sces60107, seerether, yaarduck

Labels

bug
2 (Med Risk)
downgraded by judge
satisfactory
duplicate-71

Awards

1016.4835 USDC - $1,016.48

External Links

Lines of code

https://github.com/code-423n4/2023-06-canto/blob/main/Canto/x/coinswap/keeper/swap.go#L26 https://github.com/code-423n4/2023-06-canto/blob/main/Canto/x/coinswap/keeper/swap.go#L18 https://github.com/code-423n4/2023-06-canto/blob/main/Canto/x/coinswap/keeper/swap.go#L203 https://github.com/code-423n4/2023-06-canto/blob/main/Canto/x/onboarding/keeper/ibc_callbacks.go#L93

Vulnerability details

Impact

The onboarding process calls k.coinswapKeeper.TradeInputForExactOutput if needed. There should be two consequences: Either a small portion of the transferred tokens will be swapped so the user receives 4 Canto tokens if the swap succeeds. Or nothing changed if the swap fails. However, swap.swapCoins could cause the user to lose their token without receiving 4 Canto.

Proof of Concept

OnRecvPacket calls k.coinswapKeeper.TradeInputForExactOutput if needed. https://github.com/code-423n4/2023-06-canto/blob/main/Canto/x/onboarding/keeper/ibc_callbacks.go#L93

func (k Keeper) OnRecvPacket(
	ctx sdk.Context,
	packet channeltypes.Packet,
	ack exported.Acknowledgement,
) exported.Acknowledgement {
	…

	if standardCoinBalance.LT(autoSwapThreshold) {
		swappedAmount, err = k.coinswapKeeper.TradeInputForExactOutput(ctx, coinswaptypes.Input{Coin: transferredCoin, Address: recipient.String()}, coinswaptypes.Output{Coin: swapCoins, Address: recipient.String()})
	…
}

In swap.TradeInputForExactOutput, it calls swap.swap to do the swap. https://github.com/code-423n4/2023-06-canto/blob/main/Canto/x/coinswap/keeper/swap.go#L203

func (k Keeper) TradeInputForExactOutput(ctx sdk.Context, input types.Input, output types.Output) (sdk.Int, error) {
	…

	if err := k.swapCoins(ctx, inputAddress, outputAddress, soldToken, output.Coin); err != nil {
		return sdk.ZeroInt(), err
	}

	return soldTokenAmt, nil
}

And in swap.swapCoins, we can find out that it first transfers the token from the sender to the pool and then transfers canto from the pool to the recipient. https://github.com/code-423n4/2023-06-canto/blob/main/Canto/x/coinswap/keeper/swap.go#L26 https://github.com/code-423n4/2023-06-canto/blob/main/Canto/x/coinswap/keeper/swap.go#L18

func (k Keeper) swapCoins(ctx sdk.Context, sender, recipient sdk.AccAddress, coinSold, coinBought sdk.Coin) error {
	lptDenom, err := k.GetLptDenomFromDenoms(ctx, coinSold.Denom, coinBought.Denom)
	if err != nil {
		return err
	}

	poolAddr := types.GetReservePoolAddr(lptDenom)
	if err := k.bk.SendCoins(ctx, sender, poolAddr, sdk.NewCoins(coinSold)); err != nil {
		return err
	}

	if recipient.Empty() {
		recipient = sender
	}

	return k.bk.SendCoins(ctx, poolAddr, recipient, sdk.NewCoins(coinBought))
}

Both transferal could succeed or fail. Let’s take a look at different situations:

  • Both of the transferal succeed: The swap succeeds
  • Both of the transferal fail: The swap fails and nothing changed
  • The first transferal fails: The swap fails and nothing changed
  • The first transferal succeeds but the second transferal fails: The user sends the token to the pool but the user doesn’t receive any token.

TradeInputForExactOutput doesn’t handle the last case. It simply returns sdk.ZeroInt(), err. Therefore, if the first transferal succeeds but the second transferal fails, The user loses the token without receiving any Cantos.

Tools Used

Manual Review

Add a rollback mechanism for this issue: If the first transferal succeeds but the second transferal fails, send back the token to the user.

Assessed type

Error

#0 - c4-pre-sort

2023-06-24T13:35:51Z

JeffCX marked the issue as duplicate of #5

#1 - c4-pre-sort

2023-06-24T14:07:58Z

JeffCX marked the issue as duplicate of #80

#2 - c4-judge

2023-07-03T20:52:56Z

0xean marked the issue as satisfactory

#3 - c4-judge

2023-07-03T20:53:23Z

0xean changed the severity to 2 (Med Risk)

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax Β© 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter