Amun contest - WatchPug's results

We build tokens to make it easy to invest in crypto.

General Information

Platform: Code4rena

Start Date: 13/12/2021

Pot Size: $75,000 USDC

Total HM: 11

Participants: 30

Period: 7 days

Judge: leastwood

Total Solo HM: 4

Id: 68

League: ETH

Amun

Findings Distribution

Researcher Performance

Rank: 1/30

Findings: 6

Award: $12,278.38

🌟 Selected for report: 13

🚀 Solo Findings: 1

Findings Information

🌟 Selected for report: WatchPug

Labels

bug
3 (High Risk)
sponsor confirmed

Awards

10014.677 USDC - $10,014.68

External Links

Handle

WatchPug

Vulnerability details

Under certain circumstances, e.g. annualizedFee being minted to feeBeneficiary between the time user sent the transaction and the transaction being packed into the block and causing amounts of underlying tokens for each basketToken to decrease. It's possible or even most certainly that there will be some leftover basket underlying tokens, as BasketFacet.sol#joinPool() will only transfer required amounts of basket tokens from Join contracts.

However, in the current implementation, only the leftover inputToken is returned.

As a result, the leftover underlying tokens won't be returned to the user, which constitutes users' fund loss.

https://github.com/code-423n4/2021-12-amun/blob/cf890dedf2e43ec787e8e5df65726316fda134a1/contracts/basket/contracts/singleJoinExit/SingleTokenJoinV2.sol#L57-L78

function joinTokenSingle(JoinTokenStructV2 calldata _joinTokenStruct)
    external
{
    // ######## INIT TOKEN #########
    IERC20 inputToken = IERC20(_joinTokenStruct.inputToken);

    inputToken.safeTransferFrom(
        msg.sender,
        address(this),
        _joinTokenStruct.inputAmount
    );

    _joinTokenSingle(_joinTokenStruct);

    // ######## SEND TOKEN #########
    uint256 remainingIntermediateBalance = inputToken.balanceOf(
        address(this)
    );
    if (remainingIntermediateBalance > 0) {
        inputToken.safeTransfer(msg.sender, remainingIntermediateBalance);
    }
}

https://github.com/code-423n4/2021-12-amun/blob/cf890dedf2e43ec787e8e5df65726316fda134a1/contracts/basket/contracts/facets/Basket/BasketFacet.sol#L143-L168

function joinPool(uint256 _amount, uint16 _referral)
    external
    override
    noReentry
{
    require(!this.getLock(), "POOL_LOCKED");
    chargeOutstandingAnnualizedFee();
    LibBasketStorage.BasketStorage storage bs =
        LibBasketStorage.basketStorage();
    uint256 totalSupply = LibERC20Storage.erc20Storage().totalSupply;
    require(
        totalSupply.add(_amount) <= this.getCap(),
        "MAX_POOL_CAP_REACHED"
    );

    uint256 feeAmount = _amount.mul(bs.entryFee).div(10**18);

    for (uint256 i; i < bs.tokens.length; i++) {
        IERC20 token = bs.tokens[i];
        uint256 tokenAmount =
            balance(address(token)).mul(_amount.add(feeAmount)).div(
                totalSupply
            );
        require(tokenAmount != 0, "AMOUNT_TOO_SMALL");
        token.safeTransferFrom(msg.sender, address(this), tokenAmount);
    }
    ...

Furthermore, the leftover tokens in the SingleTokenJoinV2 contract can be stolen by calling joinTokenSingle() with fake outputBasket contract and swap.exchange contract.

Recomandation

Consider changing to:

  1. Calling IBasketFacet.calcTokensForAmount() first and only swap for exactly the desired amounts of tokens (like SingleTokenJoin.sol);
  2. Or, refund leftover tokens.

Findings Information

🌟 Selected for report: pmerkleplant

Also found by: WatchPug, certora, hyh, p4st13r4, pauliax, robee

Labels

bug
duplicate
2 (Med Risk)

Awards

228.0947 USDC - $228.09

External Links

Handle

WatchPug

Vulnerability details

https://github.com/code-423n4/2021-12-amun/blob/98f6e2ff91f5fcebc0489f5871183566feaec307/contracts/basket/contracts/singleJoinExit/SingleTokenJoin.sol#L133-L137

uint256 outputAmount = outputToken.balanceOf(address(this));
require(
    outputAmount == _joinTokenStruct.outputAmount,
    "FAILED_OUTPUT_AMOUNT"
);

In the current implementation, _joinTokenSingle() requires balanceOf outputToken strictly equal to outputAmount in calldata.

However, since anyone can transfer outputToken to the contract, if there are existing outputToken in the balanceOf the contract, joinTokenSingle() will always fail with "FAILED_OUTPUT_AMOUNT".

An attacker can send 1 wei of outputToken and break joinTokenSingle().

The same problem also applies to:

https://github.com/code-423n4/2021-12-amun/blob/98f6e2ff91f5fcebc0489f5871183566feaec307/contracts/basket/contracts/singleJoinExit/SingleTokenJoinV2.sol#L128-L132

uint256 outputAmount = outputToken.balanceOf(address(this));
require(
    outputAmount == _joinTokenStruct.outputAmount,
    "FAILED_OUTPUT_AMOUNT"
);

Recomandation

Change to:

uint256 outputAmount = outputToken.balanceOf(address(this));
require(
    outputAmount >= _joinTokenStruct.outputAmount,
    "FAILED_OUTPUT_AMOUNT"
);

outputToken.safeTransfer(msg.sender, _joinTokenStruct.outputAmount);

#0 - loki-sama

2022-01-03T09:29:09Z

duplicate #81

Findings Information

🌟 Selected for report: Czar102

Also found by: WatchPug, gpersoon, gzeon, kenzo

Labels

bug
duplicate
2 (Med Risk)

Awards

394.2378 USDC - $394.24

External Links

Handle

WatchPug

Vulnerability details

https://github.com/code-423n4/2021-12-amun/blob/98f6e2ff91f5fcebc0489f5871183566feaec307/contracts/basket/contracts/facets/Basket/BasketFacet.sol#L153-L158

require(
    totalSupply.add(_amount) <= this.getCap(),
    "MAX_POOL_CAP_REACHED"
);

uint256 feeAmount = _amount.mul(bs.entryFee).div(10**18);

feeAmount should be considered. Otherwise, the new totalSupply may surpass pool cap.

#0 - loki-sama

2022-01-03T11:29:27Z

Duplicate #283

Findings Information

🌟 Selected for report: cmichel

Also found by: JMukesh, WatchPug, defsec, p4st13r4

Labels

bug
duplicate
2 (Med Risk)

Awards

394.2378 USDC - $394.24

External Links

Handle

WatchPug

Vulnerability details

Calling ERC20.transfer() without handling the returned value is unsafe.

https://github.com/code-423n4/2021-12-amun/blob/cf890dedf2e43ec787e8e5df65726316fda134a1/contracts/basket/contracts/singleJoinExit/SingleNativeTokenExitV2.sol#L104-L104

outputToken.transfer(msg.sender, outputTokenBalance);

Recommendation

Consider using OpenZeppelin's SafeERC20 library with safe versions of transfer functions.

#0 - loki-sama

2022-01-04T10:29:53Z

duplicate #232

#1 - 0xleastwood

2022-01-22T04:09:05Z

Duplicate of #192

Findings Information

🌟 Selected for report: GiveMeTestEther

Also found by: JMukesh, Ruhum, WatchPug, certora, cmichel, kenzo, p4st13r4, pauliax, robee

Labels

bug
duplicate
1 (Low Risk)

Awards

38.7989 USDC - $38.80

External Links

Handle

WatchPug

Vulnerability details

Since the introduction of transfer(), it has typically been recommended by the security community because it helps guard against reentrancy attacks. This guidance made sense under the assumption that gas costs wouldn’t change. It's now recommended that transfer() and send() be avoided, as gas costs can and will change and reentrancy guard is more commonly used.

Any smart contract that uses transfer() is taking a hard dependency on gas costs by forwarding a fixed amount of gas: 2300.

It's recommended to stop using transfer() and switch to using call() instead.

https://github.com/code-423n4/2021-12-amun/blob/cf890dedf2e43ec787e8e5df65726316fda134a1/contracts/basket/contracts/singleJoinExit/SingleNativeTokenExitV2.sol#L121-L121

msg.sender.transfer(intermediateTokenBalance);

Can be changed to:

(bool success, ) = msg.sender.call.value(intermediateTokenBalance)("");
require(success, "ETH transfer failed");

#0 - loki-sama

2022-01-03T12:22:38Z

duplicate #175

Findings Information

🌟 Selected for report: sirhashalot

Also found by: GiveMeTestEther, JMukesh, Ruhum, WatchPug, defsec, robee

Labels

bug
duplicate
1 (Low Risk)

Awards

76.0316 USDC - $76.03

External Links

Handle

WatchPug

Vulnerability details

There are many functions across the codebase that will perform an ERC20.approve() call but does not check the success return value. Some tokens do not revert if the approval failed but return false instead.

Instances include:

https://github.com/code-423n4/2021-12-amun/blob/cf890dedf2e43ec787e8e5df65726316fda134a1/contracts/basket/contracts/singleJoinExit/SingleNativeTokenExit.sol#L44-L44

token.approve(spender, uint256(-1));

https://github.com/code-423n4/2021-12-amun/blob/cf890dedf2e43ec787e8e5df65726316fda134a1/contracts/basket/contracts/singleJoinExit/SingleNativeTokenExitV2.sol#L55-L55

https://github.com/code-423n4/2021-12-amun/blob/cf890dedf2e43ec787e8e5df65726316fda134a1/contracts/basket/contracts/singleJoinExit/SingleTokenJoin.sol#L43-L43

https://github.com/code-423n4/2021-12-amun/blob/cf890dedf2e43ec787e8e5df65726316fda134a1/contracts/basket/contracts/singleJoinExit/SingleTokenJoinV2.sol#L53-L53

It is usually good to add a require-statement that checks the return value or to use something like safeApprove; unless one is sure the given token reverts in case of a failure.

#0 - loki-sama

2022-01-04T11:16:56Z

duplicate #294

Findings Information

🌟 Selected for report: kenzo

Also found by: Czar102, WatchPug, certora, cmichel, gzeon, itsmeSTYJ, robee

Labels

bug
duplicate
1 (Low Risk)

Awards

59.8749 USDC - $59.87

External Links

Handle

WatchPug

Vulnerability details

https://github.com/code-423n4/2021-12-amun/blob/98f6e2ff91f5fcebc0489f5871183566feaec307/contracts/basket/contracts/facets/Basket/BasketFacet.sol#L260-L262

totalSupply.mul(annualizedFee).div(10**18).mul(timePassed).div(
    365 days
);

Can be changed to:

totalSupply.mul(annualizedFee).mul(timePassed).div(
    365 days
).div(10**18);

Otherwise, when totalSupply and annualizedFee are smaller numbers, the resulting number can be less than expected due to precision loss.

#0 - 0xleastwood

2022-01-23T04:47:35Z

Duplicate of #155

Findings Information

🌟 Selected for report: WatchPug

Also found by: defsec, gzeon, hyh

Labels

bug
1 (Low Risk)

Awards

182.5175 USDC - $182.52

External Links

Handle

WatchPug

Vulnerability details

There are ERC20 tokens that charge fee for every transfer() or transferFrom().

In the current implementation, BasketFacet.sol##joinPool() and SingleTokenJoin.sol#joinTokenSingle() assumes that the received amount is the same as the transfer amount, and uses it for basketToken minting and token swap.

https://github.com/code-423n4/2021-12-amun/blob/98f6e2ff91f5fcebc0489f5871183566feaec307/contracts/basket/contracts/facets/Basket/BasketFacet.sol#L162-L167

https://github.com/code-423n4/2021-12-amun/blob/98f6e2ff91f5fcebc0489f5871183566feaec307/contracts/basket/contracts/singleJoinExit/SingleTokenJoin.sol#L53-L57

https://github.com/code-423n4/2021-12-amun/blob/98f6e2ff91f5fcebc0489f5871183566feaec307/contracts/basket/contracts/singleJoinExit/SingleTokenJoin.sol#L96-L102

Recommendation

Consider comparing the before and after balanceOf to get the actual transferred amount.

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