GithubHelp home page GithubHelp logo

2022-10-mover-judging's People

Contributors

evert0x avatar hrishibhat avatar rcstanciu avatar sherlock-admin avatar

Stargazers

 avatar

Watchers

 avatar  avatar  avatar

2022-10-mover-judging's Issues

rvierdiiev - HardenedTopupProxy and ExchangeProxy doesn't send change to sender if user overpaid in native token

rvierdiiev

medium

HardenedTopupProxy and ExchangeProxy doesn't send change to sender if user overpaid in native token

Summary

If user toped up with native token and overpaid more then was provided in ethValue then the change is not returned to him.

Vulnerability Detail

When user sends amount in native token then the HardenedTopupProxy is going to exhange those funds to cardTopupToken. It is done inside of ExchangeProxy.executeSwapDirect function.
HardenedTopupProxy sends all native tokens provided by user to the ExchangeProxy using that function.
Next, ExchangeProxy extracts ethValue param from data, provided by user here. Then ExchangeProxy exchanges ethValue amount of native tokens.
It's possible that user sent more native tokens than ethValue param. Then the amount msg.value - ethValue should be returned back to user, however that amount is hold by ExchangeProxy.

Impact

User lost funds.

Code Snippet

https://github.com/sherlock-audit/2022-10-mover/blob/main/cardtopup_contract/contracts/HardenedTopupProxy.sol#L335-L343
https://github.com/sherlock-audit/2022-10-mover/blob/main/cardtopup_contract/contracts/ExchangeProxy.sol#L131-L204

Tool used

Manual Review

Recommendation

Check that msg.value == ethValue or provide logic to return overpaid funds to user.

rvierdiiev - If cardTopupToken == ETH_TOKEN_ADDRESS then it's not possible to top up with native token

rvierdiiev

medium

If cardTopupToken == ETH_TOKEN_ADDRESS then it's not possible to top up with native token

Summary

If cardTopupToken == ETH_TOKEN_ADDRESS then users will not be able to top up using native token payment, because cardTopupToken is expected to be ERC20 token only.

Vulnerability Detail

In HardenedTopupProxy._processTopup there is a check that checks if user provided same token as cardTopupToken. In this case it will try to send funds to HardenedTopupProxy and then bridge it.

However if cardTopupToken == ETH_TOKEN_ADDRESS then the transfer will fail as ETH_TOKEN_ADDRESS is used for native token payment.

Why it's possible that cardTopupToken == ETH_TOKEN_ADDRESS? Because there is no checks for what cardTopupToken address can be. So it's possible for admin to set it to ETH_TOKEN_ADDRESS.

Impact

Bridging will be not possible for native token payment.

Code Snippet

        if (_token == cardTopupToken) {
            // beneficiary is msg.sender (perform static check)
            IERC20Upgradeable(_token).safeTransferFrom(_beneficiary, address(this), _amount);

            uint256 feeAmount = _amount.mul(topupFee).div(1e18);

            // bridge from _beneficiary to card L1 relay
            _bridgeAssetDirect(_amount.sub(feeAmount), _bridgeType, _bridgeTxData);

            emit CardTopup(_beneficiary, _token, _amount, _amount.sub(feeAmount), _receiverHash);
            return;
        }

Tool used

Manual Review

Recommendation

Add check to the setter method, that provided token can't be ETH_TOKEN_ADDRESS.

caventa - User can call any function of executorAddress which is dangerous

caventa

high

User can call any function of executorAddress which is dangerous

Summary

Users can call any function of executorAddress which is dangerous.

Vulnerability Detail

CallData (See ExchangeProxy.sol#L174) could be anything.

Currently, (See ExchangeProxySwap.test.js#L44) 0xec6cc0cc is passed in to execute swapTokens function (See TokenSwapExecutorMock.sol#L39)

0xec6cc0cc can be anything to call other functions as long as the remaining code (See ExchangeProxy.sol#L178-L204) can be executed successfully.

Impact

ExecuteSwapDirect could behave in an unpredictable manner because user can execute whatever functions they like.

Code Snippet

https://github.com/sherlock-audit/2022-10-mover/blob/main/cardtopup_contract/contracts/ExchangeProxy.sol#L174

https://github.com/sherlock-audit/2022-10-mover/blob/main/cardtopup_contract/test/ExchangeProxySwap.test.js#L44

https://github.com/sherlock-audit/2022-10-mover/blob/main/cardtopup_contract/contracts/testmocks/TokenSwapExecutorMock.sol#L39

https://github.com/sherlock-audit/2022-10-mover/blob/main/cardtopup_contract/contracts/ExchangeProxy.sol#L178-L204

Tool used

Manual Review

Recommendation

Rather than depending on the users to call any functions they like, we should only restrict them to calling certain functions only.

rvierdiiev - SafeAllowanceResetUpgradeable doesn't have gap array for future upgrade variables

rvierdiiev

medium

SafeAllowanceResetUpgradeable doesn't have gap array for future upgrade variables

Summary

SafeAllowanceResetUpgradeable doesn't have gap array for future upgrade variables.

Vulnerability Detail

For SafeAccessControlEnumerableUpgradeable which is upgradeable abstract contract, inheriting contracts may introduce new variables. In order to be able to add new variables to the upgradeable abstract contract without causing storage collisions, a storage gap should be added to the upgradeable abstract contract.

If no storage gap is added, when the upgradable abstract contract introduces new variables, it may override the variables in the inheriting contract.

Impact

When the upgradable abstract contract introduces new variables, it may override the variables in the inheriting contract.

Code Snippet

https://github.com/sherlock-audit/2022-10-mover/blob/main/cardtopup_contract/contracts/utils/SafeAllowanceResetUpgradeable.sol

Tool used

Manual Review

Recommendation

Consider adding a storage gap at the end of the upgradeable abstract contract like this uint256[50] private __gap;

Duplicate of #17

0x0 - Incorrect Constant Naming Could Result In Losses

0x0

medium

Incorrect Constant Naming Could Result In Losses

Summary

There is a user role that is used to permit trusted backend services to execute transactions named TRUSTED_EXETUTOR_ROLE. This is incorrectly spelled.

Vulnerability Detail

HardenedTopupProxy

The TRUSTED_EXETUTOR_ROLE is a role for trusted backend services to call functions within the topup proxy. This is a typo of "executor". Future references to this role with the correct spelling will fail.

Impact

This contract is upgradable and future functionality may reuse the same role based access control. If a future release references this role with the correct spelling, the deployer will be forced to spend Ether on a subsequent deployment to fix this typo.

Code Snippet

bytes32 public constant TRUSTED_EXETUTOR_ROLE = keccak256("TRUSTED_EXECUTION");

Tool used

Manual Review

Recommendation

Correct the typo now so that loses from re-deployments to fix misspelled constants can be avoided:

bytes32 public constant TRUSTED_EXECUTOR_ROLE = keccak256("TRUSTED_EXECUTION");

ballx - INCORRECT ACCESS CONTROL

ballx

high

INCORRECT ACCESS CONTROL

Summary

Access control plays an important role in segregation of privileges in smart contracts and other applications. If this is misconfigured or not properly validated on sensitive functions, it may lead to loss of funds, tokens and in some cases compromise of the smart contract.

The contract ExchangeProxy is importing an access control library @openzeppelin/contracts/access/AccessControl.sol but the function claimFees is missing the modifier onlyRole.

Vulnerability Detail

Impact

Code Snippet

https://github.com/sherlock-audit/2022-10-mover/blob/main/cardtopup_contract/contracts/ExchangeProxy.sol#L242-L249

    function claimFees(address _token, uint256 _amount) public {
        require(msg.sender == yieldDistributorAddress, "yield distributor only");
        if (_token != ETH_TOKEN_ADDRESS) {
            IERC20(_token).safeTransfer(msg.sender, _amount);
        } else {
            payable(msg.sender).sendValue(_amount);
        }
    }

https://github.com/sherlock-audit/2022-10-mover/blob/main/cardtopup_contract/contracts/ExchangeProxy.sol#L131-L210

    function executeSwapDirect(
        address _beneficiary,
        address _tokenFrom,
        address _tokenTo,
        uint256 _amount,
        uint256 _exchangeFee,
        bytes memory _data
    ) public payable override returns (uint256) {
        require(msg.sender == transferProxyAddress, "transfer proxy only");

        // extract values from bytes array provided
        address executorAddress;
        address spenderAddress;
        uint256 ethValue;

        bytes memory callData = ByteUtil.slice(_data, 72, _data.length - 72);
        assembly {
            executorAddress := mload(add(_data, add(0x14, 0)))
            spenderAddress := mload(add(_data, add(0x14, 0x14)))
            ethValue := mload(add(_data, add(0x20, 0x28)))
        }

        // allow spender to transfer tokens from this contract
        if (_tokenFrom != ETH_TOKEN_ADDRESS && spenderAddress != address(0)) {
            require(trustedRegistryContract.isWhitelisted(spenderAddress), "allowance to non-trusted");
            resetAllowanceIfNeeded(IERC20(_tokenFrom), spenderAddress, _amount);
        }

        // remember the actual balance of target token (fees could reside on this contract balance)
        uint256 balanceBefore = 0;
        if (_tokenTo != ETH_TOKEN_ADDRESS) {
            balanceBefore = IERC20(_tokenTo).balanceOf(address(this));
        } else {
            balanceBefore = address(this).balance;
        }

        // regardless of stated amount, the ETH value passed to exchange call must be provided to the contract
        require(msg.value >= ethValue, "insufficient ETH provided");

        // don't allow to call non-trusted addresses
        require(trustedRegistryContract.isWhitelisted(executorAddress), "call to non-trusted");

        // ensure no state passed, no reentrancy, etc.
        (bool success, ) = executorAddress.call{value: ethValue}(callData);
        require(success, "SWAP_CALL_FAILED");

        // always rely only on actual amount received regardless of called parameters
        uint256 amountReceived = 0;
        if (_tokenTo != ETH_TOKEN_ADDRESS) {
            amountReceived = IERC20(_tokenTo).balanceOf(address(this));
        } else {
            amountReceived = address(this).balance;
        }
        amountReceived = amountReceived.sub(balanceBefore);

        require(amountReceived > 0, "zero amount received");

        // process exchange fee if present (in deposit we get pool tokens, so process fees after swap, here we take fees in source token)
        // fees are left on this contract address and are harvested by yield distributor
        //uint256 feeAmount = amountReceived.mul(_exchangeFee).div(1e18);
        amountReceived = amountReceived.sub(
            amountReceived.mul(_exchangeFee).div(1e18)
        ); // this is return value that should reflect actual result of swap (for deposit, etc.)

        if (_tokenTo != ETH_TOKEN_ADDRESS) {
            //send received tokens to beneficiary directly
            IERC20(_tokenTo).safeTransfer(_beneficiary, amountReceived);
        } else {
            //send received eth to beneficiary directly
            payable(_beneficiary).sendValue(amountReceived);
            // payable(_beneficiary).transfer(amountReceived);
            // should work for external wallets (currently is the case)
            // but wont work for some other smart contracts due to gas stipend limit
        }

        emit ExecuteSwap(_beneficiary, _tokenFrom, _tokenTo, _amount, amountReceived);
        
        // amount received is used to check minimal amount condition set by calling app from topup proxy contract
        return amountReceived;
    }

https://github.com/sherlock-audit/2022-10-mover/blob/main/cardtopup_contract/contracts/ExchangeProxy.sol#L91-L103

    function executeSwap(
        address _tokenFrom,
        address _tokenTo,
        uint256 _amount,
        bytes memory _data
    ) public payable override returns (uint256) {
        // native token doesn't need to be transferred explicitly, it's in tx.value
        if (_tokenFrom != ETH_TOKEN_ADDRESS) {
            IERC20(_tokenFrom).safeTransferFrom(msg.sender, address(this), _amount);
        }
        // after token is transferred to this contract, call actual swap
        return executeSwapDirect(msg.sender, _tokenFrom, _tokenTo, _amount, 0, _data);
    }

Tool used

Manual Review

Recommendation

It is recommended to go through the contract and observe the functions that are lacking an access control modifier. If they contain sensitive administrative actions, it is advised to add a suitable modifier to the same

GalloDaSballo - M-01 `executeSwap` doesn't work

GalloDaSballo

medium

M-01 executeSwap doesn't work

Summary

Due to most likely a programming mistake, executeSwap will call executeSwapDirect and then revert.

Vulnerability Detail

Impact

Because executeSwap calls executeSwapDirect it will always revert.

HardenedTopUpProxy is not programmed to call it either, leading me to believe this is a programming mistake.

Code Snippet

https://github.com/sherlock-audit/2022-10-mover/blob/main/cardtopup_contract/contracts/ExchangeProxy.sol#L91-L92

    function executeSwap(

Tool used

Manual Review

Recommendation

Delete the code if unused.
Or refactor to allow any caller to use it.

0x0 - Fixed Decimal Fee Calculation

0x0

medium

Fixed Decimal Fee Calculation

Summary

Top ups are being processed with the assumption that the token always has 18 decimals.

Vulnerability Detail

HardenedTopupProxy._processTopup

The fee is calculated with the assumption that the token has 18 decimals. The calculation is incorrect for tokens that do not use 18 decimals.

Impact

Should an asset be added to the system that does not use 18 decimals, such as Tether, the top up fee will be incorrect.

Code Snippet

uint256 feeAmount = _amount.mul(topupFee).div(1e18);

Tool used

Manual Review

Recommendation

Consider whether the addition of tokens with non-standard number of decimals is desirable in the future. If so, refactor this function to accommodate assets that do not use 18 decimals.

Duplicate of #6

seyni - Missing check in `ExchangeProxy.executeSwapDirect` lead to users potentially losing a significant amount of their assets

seyni

high

Missing check in ExchangeProxy.executeSwapDirect lead to users potentially losing a significant amount of their assets

Summary

If ExchangeProxy.executeSwapDirect is called from ExchangeProxy.executeSwap users could lose a significant amount of value of their assets.

Vulnerability Detail

When ExchangeProxy.executeSwapDirect is called from ExchangeProxy.executeSwap, there is no check to verify that the amountReceived hasn't considerably decreased compared to a fair amount, instead there is only a check for amountReceived > 0:
https://github.com/sherlock-audit/2022-10-mover/blob/main/cardtopup_contract/contracts/ExchangeProxy.sol#L186

        require(amountReceived > 0, "zero amount received");

The missing check is done in HardenedTopupProxy like this:
https://github.com/sherlock-audit/2022-10-mover/blob/main/cardtopup_contract/contracts/HardenedTopupProxy.sol#L346

        require(amountReceived >= _expectedMinimumReceived, "minimum swap amount not met");

But is never done if ExchangeProxy.executeSwapDirect is used through ExchangeProxy.executeSwap.

Impact

When ExchangeProxy.executeSwapDirect is called from ExchangeProxy.executeSwap, a swap could go through with an user losing a significant amount of value of his assets.

Code Snippet

https://github.com/sherlock-audit/2022-10-mover/blob/main/cardtopup_contract/contracts/ExchangeProxy.sol#L186

        require(amountReceived > 0, "zero amount received");

Tool used

Manual Review

Recommendation

The following check:

        require(amountReceived >= _expectedMinimumReceived, "minimum swap amount not met");

Could be done in ExchangeProxy.executeSwapDirect instead of HardenedTopupProxy._processTopup which would allow both call from ExchangeProxy.executeSwap and HardenedTopupProxy._processTopup to be covered by the check.
The value of _expectedMinimumReceived would be sent in _data.

JohnSmith - Fee-on-transfer tokens are not supported

JohnSmith

medium

Fee-on-transfer tokens are not supported

Summary

The protocoll will fail to exchange FoT tokens, because transfered amount is often less than received.

Vulnerability Detail

Plenty of ERC20 tokens charge a fee for every transfer (e.g. Safemoon and its forks), in which the amount of token received is less than the amount being sent. When a fee token is used as the _token in the _processTopup() function, the amount received by the contract would be less than the amount being sent.
Let's assume that _amount is 100.
After transfer

cardtopup_contract/contracts/HardenedTopupProxy.sol
331:             IERC20Upgradeable(_token).safeTransferFrom(_beneficiary, address(exchangeProxyContract), _amount);

We receive for example 95.
However we tell our Exchange proxy to swap 100:

cardtopup_contract/contracts/HardenedTopupProxy.sol
335:         uint256 amountReceived =
336:             IExchangeProxy(address(exchangeProxyContract)).executeSwapDirect{value: msg.value}(
337:                 address(this),
338:                 _token,
339:                 cardTopupToken,
340:                 _amount,//@audit fee on transfer may fail or use accumulated fees to proceed
341:                 exchangeFee,
342:                 _convertData
343:             );

When it calls an exchange, exchange will try to transferFrom our contract 100, but we only have 95, so it will fail, or use some tokens transfered before for some reason.

Impact

FoT tokens are not supported. Or just drained if such tokens were provided by someone.

Code Snippet

https://github.com/sherlock-audit/2022-10-mover/blob/main/cardtopup_contract/contracts/HardenedTopupProxy.sol#L331
https://github.com/sherlock-audit/2022-10-mover/blob/main/cardtopup_contract/contracts/HardenedTopupProxy.sol#L335-L343

Tool used

Manual Review

Recommendation

Check how many tokens are actually received

cardtopup_contract/contracts/HardenedTopupProxy.sol
+	uint balanceBefore = IERC20Upgradeable(_token).balanceOf(address(exchangeProxyContract));
331:   IERC20Upgradeable(_token).safeTransferFrom(_beneficiary, address(exchangeProxyContract), _amount);
+   	uint balanceAfter = IERC20Upgradeable(_token).balanceOf(address(exchangeProxyContract));
+   	uint received = balanceAfter - balanceBefore;

and then

cardtopup_contract/contracts/HardenedTopupProxy.sol
335:         uint256 amountReceived =
336:             IExchangeProxy(address(exchangeProxyContract)).executeSwapDirect{value: msg.value}(
337:                 address(this),
338:                 _token,
339:                 cardTopupToken,
- 340:                 _amount,
+ 340:                 received,
341:                 exchangeFee,
342:                 _convertData
343:             );

Duplicate of #39

0x0 - Card Top Up Replay Attack

0x0

medium

Card Top Up Replay Attack

Summary

Card top ups facilitated from trusted infrastructure call a function in the Top Up Proxy. In the current implementation it is possible to replay top up transactions.

Vulnerability Detail

HardenedTopupProxy.CardTopupTrusted

This implements functionality to process a card top up from a trusted backend system. There is the possibility under the following circumstances for a malicious actor to replay this transaction and top up a card multiple times from the same signature:

  • allowanceSignatureTimespan must be set to a figure greater than 0 as transaction inclusion into a block is non-deterministic. If the sender does not know when their transaction will be included this provides a second based buffer to allow the sender to provide a function argument of the current time in seconds. This is intended as a mechanism to prevent replay attacks by giving a window where the signature should be accepted.
  • Assume the backend has been compromised. An attacker has access to the key material that grants access to TRUSTED_EXETUTOR_ROLE.
  • A transaction is received and the backend replays this transaction multiple times by calling CardTopupTrusted repeatedly with the signature it has received.

Impact

  • This causes multiple card top ups from a single top up signature. A user topping up their card has the transaction replayed multiple times and will succeed for as long as there is both a balance and allowance.

Code Snippet

require(block.timestamp - _timestamp < allowanceSignatureTimespan, "old sig");

Tool used

Manual Review

Recommendation

Implement a cryptographic nonce into the signature and the contracts to prevent the same signature being used more than once.

Duplicate of #42

aviggiano - CardTopupTrusted is vulnerable against replay attacks on eventual chain forks

aviggiano

high

CardTopupTrusted is vulnerable against replay attacks on eventual chain forks

Summary

The function CardTopupTrusted of the contract HardenedTopupProxy is vulnerable against replay attacks on eventual chain forks due to lack of block.chainid on the signed message from the trusted backend.

Vulnerability Detail

In case of a chain split, signed messages from the trusted backend would still be valid on the forked chain since constructMsg does not encode block.chainid on the message to be hashed and signed. This might lead an attacker to process the topup on both chains and spend the users' allowance on the forked chain without their consent.

Impact

High

Code Snippet

    /**
        @dev reconstruct a message to be verified (signed by trusted backend) that allowance is recent and of correct value
     */
    function constructMsg(bytes32 _addrhash, address _token, uint256 _amount, uint256 _timestamp) internal pure returns (bytes32) {
        return keccak256(abi.encodePacked("MOVER TOPUP ", _addrhash, " TOKEN ", _token, " AMOUNT ", _amount, " TS ", _timestamp));
    }

https://github.com/sherlock-audit/2022-10-mover/blob/main/cardtopup_contract/contracts/HardenedTopupProxy.sol#L450-L455

Tool used

Manual Review

Recommendation

  • Implement EIP 712 for hashing and signing of typed structured data with the proper domain separator being updated in case of a chain split.
  • Make use of OpenZeppelin's EIP712.sol smart contract for the constructMsg function

Duplicate of #42

Bnke0x0 - Should prevent users from sending more native tokens in theย executeSwapDirect function

Bnke0x0

medium

Should prevent users from sending more native tokens in theย executeSwapDirect function

Summary

It is possible for a user purchasing an option to accidentally overpay

Vulnerability Detail

Impact

When a user bridges a native token via theย executeSwapDirect ย function ofย ExchangeProxy, the contract checks whetherย msg.value >= ethValueย holds. In other words, if a user accidentally sends more native tokens than he has to, the contract accepts it but only bridges theย ethValueย amount of tokens. The rest of the tokens are left in the contract and can be recovered by anyone (see another submission for details).

Code Snippet

https://github.com/sherlock-audit/2022-10-mover/blob/main/cardtopup_contract/contracts/ExchangeProxy.sol#L168

  'require(msg.value >= ethValue, "insufficient ETH provided");'

Tool used

Manual Review

Recommendation

Consider changingย >=ย toย ==ย at line 168.
https://github.com/sherlock-audit/2022-10-mover/blob/main/cardtopup_contract/contracts/ExchangeProxy.sol#L168

  'require(msg.value == ethValue, "insufficient ETH provided");'

JohnSmith - No Storage Gap for Upgradeable Contract Might Lead to Storage Slot Collision

JohnSmith

medium

No Storage Gap for Upgradeable Contract Might Lead to Storage Slot Collision

Summary

For upgradeable contracts, there must be storage gap to โ€œallow developers to freely add new state variables in the future without compromising the storage compatibility with existing deploymentsโ€ (quote OpenZeppelin). Otherwise it may be very difficult to write new implementation code.

Vulnerability Detail

Without storage gap, the variable in child contract might be overwritten by the upgraded base contract if new variables are added to the base contract.
Refer to the bottom part of this article: https://docs.openzeppelin.com/upgrades-plugins/1.x/writing-upgradeable

The storage gap is essential for upgradeable contract because โ€œIt allows us to freely add new state variables in the future without compromising the storage compatibility with existing deploymentsโ€. Refer to the bottom part of this article:

https://docs.openzeppelin.com/contracts/3.x/upgradeable

Impact

If the contract inheriting the base contract contains additional variable, then the base contract cannot be upgraded to include any additional variable, because it would overwrite the variable declared in its child contract. This greatly limits contract upgradeability.
This could have unintended and very serious consequences to the child contracts, potentially causing loss of funds or cause the contract to malfunction completely.

Code Snippet

HardenedTopupProxy is an upgradable contract which extends AccessControlUpgradeable and SafeAllowanceResetUpgradeable

contracts/HardenedTopupProxy.sol
68: contract HardenedTopupProxy is AccessControlUpgradeable, SafeAllowanceResetUpgradeable {

When AccessControlUpgradeable has such gap:

node_modules/@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol
259:     uint256[49] private __gap;
contracts/utils/SafeAllowanceResetUpgradeable.sol
9: abstract contract SafeAllowanceResetUpgradeable {

does not.

Tool used

Manual Review

Recommendation

Add uint256[50] private __gap; to base contracts, which are upgradable/inherted by upgradable contracts.

Miguel - _beneficiary parameter value is not checked

Miguel

medium

_beneficiary parameter value is not checked

Summary

_beneficiary address is not checked when executeSwapDirect method is executed. This address must be validated to be not equals to address(0).

Vulnerability Detail

If _beneficiary address is equals to address(0) then it could cause to burn/loose funds due to mistake in executeSwapDirect method.
Despite executeSwapDirect routine is called from executeSwap it is open to be called from outside (is declared public). So it should be checked anyway.

Impact

Fees could not be sent to _beneficiary in case it is address(0)

Code Snippet

https://github.com/sherlock-audit/2022-10-mover/blob/main/cardtopup_contract/contracts/ExchangeProxy.sol#L195-L204

Tool used

Manual Review

Recommendation

The recommendation is:

  • Validate _beneficiary address doing something like that:
  • Validate if there is some value to send. It is because if you do not have something to send, it is not necessary to perform next validation.
  • Add require sentence before sending amounts to _beneficiary to validate it is different than address(0).

Take a look the following POC code:

`
if (amountReceived > 0)

    {
        require(_beneficiary != address(0), "_beneficiary is zero address");
        if (_tokenTo != ETH_TOKEN_ADDRESS) {
            //send received tokens to beneficiary directly
            IERC20(_tokenTo).safeTransfer(_beneficiary, amountReceived);
        } else {
            //send received eth to beneficiary directly
            payable(_beneficiary).sendValue(amountReceived);
            // payable(_beneficiary).transfer(amountReceived);
            // should work for external wallets (currently is the case)
            // but wont work for some other smart contracts due to gas stipend limit
        }
    }

`

yixxas - Use of ecrecover opens up protocol to signature replay attack

yixxas

medium

Use of ecrecover opens up protocol to signature replay attack

Summary

Message signed does not contain anything that prevents reuse of the same signature. This opens up the protocol to signature replay attack for when signer is used to verify a transaction.

Vulnerability Detail

In the message, bytes32 message = constructMsg(keccak256(abi.encodePacked(msg.sender)), _token, _amount, _timestamp), it contains only the address of msg.sender, token address, _amount and _timestamp, none of which can prevent the attacker from using the same signature multiple times, as long as different transactions are made in the same block so _timestamp remains the same.

Impact

CardTopupTrusted() relies on the signature to verify that a user is allowed to make a top up after receiving approval by a trusted party. This can allow a user to make more top ups than he is allowed to which may create problems if the trusted party only allows user to top up once.

Code Snippet

https://github.com/sherlock-audit/2022-10-mover/blob/main/cardtopup_contract/contracts/HardenedTopupProxy.sol#L1033
https://github.com/sherlock-audit/2022-10-mover/blob/main/cardtopup_contract/contracts/HardenedTopupProxy.sol#L1031

Tool used

Manual Review

Recommendation

A nonce should be used as part of the message to prevent such an attack. However, use of OpenZeppelin's ECDSA library is recommended which includes many other important checks.

Duplicate of #42

csanuragjain - Decimal difference can give incorrect amount to user

csanuragjain

high

Decimal difference can give incorrect amount to user

Summary

The decimal places of _tokenTo is not considered before multiplying it with _exchangeFee which means the final amount might be incorrect

Vulnerability Detail

  1. Observe the executeSwapDirect function
function executeSwapDirect(
        address _beneficiary,
        address _tokenFrom,
        address _tokenTo,
        uint256 _amount,
        uint256 _exchangeFee,
        bytes memory _data
    ) public payable override returns (uint256) {
...
if (_tokenTo != ETH_TOKEN_ADDRESS) {
            amountReceived = IERC20(_tokenTo).balanceOf(address(this));
        }
amountReceived = amountReceived.sub(
            amountReceived.mul(_exchangeFee).div(1e18)
        ); 
...
}
  1. Here _tokenTo might be an erc20 token with 10 decimal places and without normalizing it is directly multiplied with _exchangeFee (18 decimal places) which will give incorrect final received amount

Impact

User could receive lesser funds than required in case _tokenTo has very few decimal places

Code Snippet

https://github.com/sherlock-audit/2022-10-mover/blob/main/cardtopup_contract/contracts/ExchangeProxy.sol#L191

Tool used

Manual Review

Recommendation

Normalize the decimal places to 18 for _tokenTo

ballx - CONTROLLED DELEGATE CALL

ballx

high

CONTROLLED DELEGATE CALL

Summary

The contract was using delegatecall() or call() which was accepting address controlled by a user. This can have devastating effects on the contract as a delegate call allows the contract to execute code belonging to other contracts but using itโ€™s own storage. This can very easily lead to a loss of funds and compromise of the contract

Vulnerability Detail

Impact

Code Snippet

https://github.com/sherlock-audit/2022-10-mover/blob/main/cardtopup_contract/contracts/ExchangeProxy.sol#L174

(bool success, ) = executorAddress.call{value: ethValue}(callData);

https://github.com/sherlock-audit/2022-10-mover/blob/main/cardtopup_contract/contracts/HardenedTopupProxy.sol#L1010

 (bool success, /*bytes memory result*/) = address(_token).call(abi.encodePacked(IERC20PermitUpgradeable.permit.selector, _permit));

Tool used

Manual Review

Recommendation

Do not allow user-controlled data inside the delegatecall() and the call() function.

Miguel - yieldDistributorAddress' fees could be stolen by admin

Miguel

medium

yieldDistributorAddress' fees could be stolen by admin

Summary

Admin users have the privilege to change yieldDistributorAddress that is correct but It could cause that previous yieldDistributor loose its fees.

Vulnerability Detail

Assumption:
Previous yieldDistributor has a big amount in fees to claim.

Check the following steps

  • Original yieldDistributor has more 100 ETH (or any big amount) in fees to claim.
  • Admin decided to set a new yieldDistributor,
  • New yieldDistributor claims for the fees that belongs to the previous one.

I mean that admin in the unique failure point in the process. So you are only trusting in the admins good intentions but it could be broken due to big amounts of money. It could happen if some private key's admin are stolen even.

The code does not protect (and should from my point of view) to the original yieldDistributor which is an interested actor in the process.

Impact

ExchangeProxy.sol

Code Snippet

setYieldDistributor method:
https://github.com/sherlock-audit/2022-10-mover/blob/main/cardtopup_contract/contracts/ExchangeProxy.sol#L224-L229

claimFees method:
https://github.com/sherlock-audit/2022-10-mover/blob/main/cardtopup_contract/contracts/ExchangeProxy.sol#L242-L249

Tool used

Manual Review

Recommendation

I suggest two possible solutions (or security improvement to the process):
1- Delay claim fees.

  • Set a contract field with block number like currentBlock + 5760 (~1 day).
  • Create a condition in claimFees method for currentBlock > enableBlockToClaimFee.

2- Validate claimed fee in setYieldDistributor.

  • If remaining fees to claim is higher than some amount (1 ETH for instance), then revert new yield distributor assignation to avoid some malicious action.

Consider that adding some of the previous suggestions will make the protocol more incorruptible.

Finally, I recommend to emit some event when setting new yieldDistributor.

seyni - `cardPartnerAddress` and `allowanceSignatureTimespan` aren't initialized in the constructor in HardenedTopupProxy.sol

seyni

medium

cardPartnerAddress and allowanceSignatureTimespan aren't initialized in the constructor in HardenedTopupProxy.sol

Summary

cardPartnerAddress and allowanceSignatureTimespan aren't initialized in the constructor. If the admin were to unpause the contract without initializing these variables, it could lead to early users losing funds and for HardenedTopupProxy.CardTopupTrusted not to be usable.

Vulnerability Detail

Both variables cardPartnerAddress and allowanceSignatureTimespan aren't initialized in the constructor. The admin could unpaused the contract without initializing them.
cardPartnerAddress is the L1 Eth address for card topup settlement, therefore if this variable is not initialized early users would send funds to address(0) on L1.
allowanceSignatureTimespan is allowance max signature age, therefore if this variable is not initialized the HardenedTopupProxy.CardTopupTrusted would always revert.

Impact

  • Early users would be sending their funds to address(0) on L1 if cardPartnerAddress isn't initialized.
  • HardenedTopupProxy.CardTopupTrusted would be unusable if allowanceSignatureTimespan isn't initialized

I think this issue represent a Medium Risk and not a High Risk, because the admin could pause the contract or update the variables when
the issue is discovered. Especially, I think it still represent a Medium Risk because this is a possible state of the contract.

Code Snippet

https://github.com/sherlock-audit/2022-10-mover/blob/main/cardtopup_contract/contracts/HardenedTopupProxy.sol#L1041

        require(block.timestamp - _timestamp < allowanceSignatureTimespan, "old sig");

https://github.com/sherlock-audit/2022-10-mover/blob/main/cardtopup_contract/contracts/HardenedTopupProxy.sol#L432

            IAcrossBridgeSpokePool(targetAddress).deposit(cardPartnerAddress,

Tool used

Manual Review

Recommendation

I recommend initializing these variables to a trusted default address for cardPartnerAddress and to a reasonable default amount of time for allowanceSignatureTimespan.

GalloDaSballo - M-03 Blockhash doesn't work for current block

GalloDaSballo

medium

M-03 Blockhash doesn't work for current block

Summary

https://github.com/sherlock-audit/2022-10-mover/blob/main/cardtopup_contract/contracts/HardenedTopupProxy.sol#L1058-L1059

Should check that block provided is less than current as the current blockHash cannot be known

Vulnerability Detail

Checking for blockhash(block.number) will always return 0

Impact

Am not sure value can be stolen via this

Code Snippet

// SPDX-License-Identifier: MIT
pragma solidity 0.8.10;

contract MoverTests {
  function prooveBlockhash(uint256 blockNumberDelta, bytes32 expected) external {
    uint256 blockTarget = block.number - blockNumberDelta;
    require(expected == blockhash(blockTarget), "Hash is not expected");
  }
}

If the value was non-zero we'd get a revert

>>> c.prooveBlockhash(0, 0, {"from": a[0]})
Transaction sent: 0x3a9d4c6aab7c94651a2176fdd75ea3ead6cc7f94408b34bfa480a26f090d5769
  Gas price: 0.0 gwei   Gas limit: 12000000   Nonce: 7
  MoverTests.prooveBlockhash confirmed   Block: 15820123   Gas used: 21665 (0.18%)

<Transaction '0x3a9d4c6aab7c94651a2176fdd75ea3ead6cc7f94408b34bfa480a26f090d5769'>
>>>

However we do not, meaning a 0 blockhash will be provided

Tool used

Manual Review

Recommendation

Prevent using current block

8olidity - ECRECOVER() NOT CHECKED FOR SIGNER ADDRESS OF ZERO

8olidity

medium

ECRECOVER() NOT CHECKED FOR SIGNER ADDRESS OF ZERO

Summary

ECRECOVER() NOT CHECKED FOR SIGNER ADDRESS OF ZERO

Vulnerability Detail

ECRECOVER() NOT CHECKED FOR SIGNER ADDRESS OF ZERO

Impact

The ecrecover() function returns an address of zero when the signature does not match. This can cause problems if address zero is ever the owner of assets, and someone uses the permit function on address zero. If that happens, any invalid signature will pass the checks, and the assets will be stealable. In this case, the asset of concern is the vaultโ€™s ERC20 token, and fortunately OpenZeppelinโ€™s implementation does a good job of making sure that address zero is never able to have a positive balance. If this contract ever changes to another ERC20 implementation that is laxer in its checks in favor of saving gas, this code may become a problem.

Code Snippet

https://github.com/sherlock-audit/2022-10-mover/blob/main/cardtopup_contract/contracts/HardenedTopupProxy.sol#L460-L484

    function recoverSigner(bytes32 message, bytes memory sig) internal pure returns (address)
    {
        // signature is expected to be exactly 65 bytes (2 * 32 byte words and a checksum)
        require(sig.length == 65, "invalid sig length");

        // signature components
        bytes32 r;
        bytes32 s;
        uint8 v;
        assembly {
            // first 32 bytes, after the length prefix
            r := mload(add(sig, 32))
            // second 32 bytes
            s := mload(add(sig, 64))
            // final byte (first byte of the next 32 bytes)
            v := byte(0, mload(add(sig, 96)))
        }

        // Version of signature should be 27 or 28, but 0 and 1 are also possible versions
        if (v < 27) {
            v += 27;
        }

        return ecrecover(message, v, r, s);
    }

Tool used

Manual Review

Recommendation

address signer = recoverSigner(message, _signature);
require(signer != address(0));

csanuragjain - Correct allowance will be rejected

csanuragjain

medium

Correct allowance will be rejected

Summary

If allowance is equal to _amount.mul(allowanceTreshold).div(1e18) then it will get rejected instead of getting accepted.

Vulnerability Detail

  1. Observe the checkAllowance function
function checkAllowance(address _token, uint256 _amount) view internal {
        require(IERC20Upgradeable(_token).allowance(msg.sender, address(this)) >= _amount, "insufficient allowance");
        require(IERC20Upgradeable(_token).allowance(msg.sender, address(this)) < _amount.mul(allowanceTreshold).div(1e18), "excessive allowance");
    }
  1. As per comment the current approval is at least the planned topup value, but not larger than some threshold. Although in this case the equality check is missing and if allowance = amount.mul(allowanceTreshold).div(1e18) then transaction fails with excessive allowance

Impact

multiple function depending on checkAllowance function will fail to work

Code Snippet

https://github.com/sherlock-audit/2022-10-mover/blob/main/cardtopup_contract/contracts/HardenedTopupProxy.sol#L288

Tool used

Manual Review

Recommendation

Revise the condition as shown below:

require(IERC20Upgradeable(_token).allowance(msg.sender, address(this)) <= _amount.mul(allowanceTreshold).div(1e18), "excessive allowance");

berndartmueller - Previous yield distributor can drain collected fees

berndartmueller

high

Previous yield distributor can drain collected fees

Summary

The yield distributor has the maximum token spending allowance for the HardenedTopupProxy.sol and ExchangeProxy contracts. However, when setting a new and different yield distributor, the old yield distributor still has the maximum spending allowance and can drain all collected fees.

Vulnerability Detail

When setting a new yield distributor with the setYieldDistributor function in both the HardenedTopupProxy.sol and ExchangeProxy contracts, the yield distributor address will have the ERC-20 token with the address _tokenAddress approved with the maximum allowance ALLOWANCE_SIZE = type(uint256).max as a spender.

If the contract admin changes the current yield distributor to a different address, the previous address still has the spending allowance. This means that the previous yield distributor can spend the tokens from both contracts.

Impact

A previously set yield distributor continues to have the ERC-20 token spending allowance and can drain the collected fees.

Code Snippet

ExchangeProxy.setYieldDistributor

function setYieldDistributor(address _tokenAddress, address _distributorAddress) public onlyAdmin {
    yieldDistributorAddress = _distributorAddress;
    // only yield to be redistributed should be present on this contract in baseAsset (or other tokens if swap fees)
    // so no access to lp tokens for the funds invested
    resetAllowanceIfNeeded(IERC20(_tokenAddress), _distributorAddress, ALLOWANCE_SIZE);
}

HardenedTopupProxy.setYieldDistributor

function setYieldDistributor(address _tokenAddress, address _distributorAddress) public onlyAdmin {
    yieldDistributorAddress = _distributorAddress;
    // only yield to be redistributed should be present on this contract balance in baseAsset
    resetAllowanceIfNeeded(IERC20Upgradeable(_tokenAddress), _distributorAddress, ALLOWANCE_SIZE);
}

Tool Used

Manual Review

Recommendation

Consider removing the call to the resetAllowanceIfNeeded function within the two setYieldDistributor functions and only let the current yield distributor claim fees via the provided claimFees function.

csanuragjain - Excess ETH is not refunded

csanuragjain

medium

Excess ETH is not refunded

Summary

It was observed that executeSwapDirect function is not refunding any excess eth which has been mistakenly passed by the user

Vulnerability Detail

  1. Observe the executeSwapDirect function
  2. Observe even if user has provided msg.value>ethValue then also contract will not be refunding msg.value-ethValue back to the user

Impact

User can lose their eth

Code Snippet

https://github.com/sherlock-audit/2022-10-mover/blob/main/cardtopup_contract/contracts/ExchangeProxy.sol#L168

Tool used

Manual Review

Recommendation

Refund any excess eth (msg.value-ethValue) which user has accidentally provided while calling executeSwapDirect function

rvierdiiev - Fee amount is not clear and can be changed in any moment for any value

rvierdiiev

medium

Fee amount is not clear and can be changed in any moment for any value

Summary

There is no any checkings for the fees that are used through the protocol.

Vulnerability Detail

Mover collects fees for exchanging tokens and bridging them. It's possible for admin to set both those variables. However there is no any checks, so admin can provide any value.

Impact

Admin can change fees to any value.

Code Snippet

https://github.com/sherlock-audit/2022-10-mover/blob/main/cardtopup_contract/contracts/HardenedTopupProxy.sol#L199-L207

Tool used

Manual Review

Recommendation

Add some bounds for the fee amount. Or at least check that it's not more than 100%.

Duplicate of #123

csanuragjain - No provision to pause ExchangeProxy

csanuragjain

medium

No provision to pause ExchangeProxy

Summary

It was observed that unlike HardenedTopupProxy, ExchangeProxy does not have a pause functionality and executeSwap can run even in worst scenarios

Vulnerability Detail

  1. Observe that ExchangeProxy contract does not have pausing capability allowing anyone to call executeSwapDirect even when owner want to pause the functionality due to an exploit

Impact

No way to pause the swapping

Code Snippet

https://github.com/sherlock-audit/2022-10-mover/blob/main/cardtopup_contract/contracts/ExchangeProxy.sol#L131

Tool used

Manual Review

Recommendation

Add pausing functionality and allow admin to pause executeSwapDirect/executeSwap whenever required

berndartmueller - The time-dependent signature check is not safe

berndartmueller

medium

The time-dependent signature check is not safe

Summary

The HardenedTopupProxy.CardTopupTrusted function is called by a trusted off-chain executor and uses a time-dependent signature check. This is unsafe as the signature can be (accidentally) reused within the allowed allowanceSignatureTimespan timespan.

Vulnerability Detail

The HardenedTopupProxy.CardTopupTrusted function is called by a trusted off-chain executor and uses a time-dependent signature check by incorporating a timestamp _timestamp in the signed signature. To prevent replay attacks, the _timestamp is checked to be within the allowed allowanceSignatureTimespan timespan. However, it is in the possible realm of the off-chain trusted executor to retry the call (due to various reasons) with the same parameters (given that the token spending allowance is set as well, or, the allowance is already set high enough to cover multiple topups with the same amount - as long as allowanceTreshold is set accordingly).

Impact

The trusted off-chain executor can use the same signature multiple times within the allowed allowanceSignatureTimespan timespan. This can lead to repeated top-ups for the receiver _receiverHash.

Code Snippet

HardenedTopupProxy.sol#L1041

function CardTopupTrusted(address _token, uint256 _amount, uint256 _timestamp, bytes calldata _signature, uint256 _expectedMinimumReceived, bytes memory _convertData, uint256 _bridgeType, bytes memory _bridgeTxData, bytes32 _receiverHash) public {
    // reconstruct message from the data used for topup to ensure it matches provided information
    bytes32 message = constructMsg(keccak256(abi.encodePacked(msg.sender)), _token, _amount, _timestamp);

    // recover signer which must be trusted executor EOA
    address signer = recoverSigner(message, _signature);
    require(hasRole(TRUSTED_EXETUTOR_ROLE, signer), "wrong signature");

    // if signature is old, don't accept it to avoid reuse and ensure approval was fresh
    // trusted executor won't produce messages with timestamps in the future
    require(block.timestamp - _timestamp < allowanceSignatureTimespan, "old sig");

    // check current allowance, regardless of the signed message vailidity
    checkAllowance(_token, _amount);

    // allowance checks/enforcing complete, perform actual topup (this flow is further unified across 3 possible topup public methods)
    _processTopup(msg.sender, _token, _amount, _expectedMinimumReceived, _convertData, _bridgeType, _bridgeTxData, _receiverHash);
}

Tool Used

Manual Review

Recommendation

Consider using a nonce to prevent replay attacks. This can be done by adding a nonce to the signed message instead of the timestamp _timestamp. The nonce can be stored and incremented on every CardTopupTrusted function call. This way the same signature can not be used twice.

berndartmueller - The yield distributor can transfer accidentally sent funds

berndartmueller

medium

The yield distributor can transfer accidentally sent funds

Summary

The yield distributor can repurpose the claimFees function to transfer accidentally sent funds to itself.

Vulnerability Detail

Both the ExchangeProxy and HardenedTopupProxy contracts have a function emergencyTransfer to allow an admin to rescue any ERC20 tokens accidentally sent to the contracts.

However, a yield distributor can also transfer those funds via the claimFees function. The claimFees function is intended to be used by the yield distributor to claim collected fees. As the contracts do not keep track of the fees collected, the yield distributor is able to claim and transfer any amount of ERC-20 tokens and native tokens, effectively stealing funds.

Impact

The yield distributor can transfer any ERC-20 tokens and native tokens accidentally sent to the contracts besides the fees collected.

Code Snippet

ExchangeProxy.sol#L242-L263

function claimFees(address _token, uint256 _amount) public {
    require(msg.sender == yieldDistributorAddress, "yield distributor only");
    if (_token != ETH_TOKEN_ADDRESS) {
        IERC20(_token).safeTransfer(msg.sender, _amount);
    } else {
        payable(msg.sender).sendValue(_amount);
    }
}

/**
    @dev all contracts that do not hold funds have this emergency function if someone occasionally
      transfers ERC20 tokens directly to this contract
      callable only by owner (admin)
*/
function emergencyTransfer(address _token, address _destination, uint256 _amount) public onlyAdmin {
    if (_token != ETH_TOKEN_ADDRESS) {
        IERC20(_token).safeTransfer(_destination, _amount);
    } else {
        payable(_destination).sendValue(_amount);
    }
    emit EmergencyTransfer(_token, _destination, _amount);
}

HardenedTopupProxy.sol#L248-L273

function claimFees(address _token, uint256 _amount) public {
    require(msg.sender == yieldDistributorAddress, "yield distributor only");
    if (_token != ETH_TOKEN_ADDRESS) {
        IERC20Upgradeable(_token).safeTransfer(msg.sender, _amount);
    } else {
        payable(msg.sender).sendValue(_amount);
    }
}

/**
    @dev all Mover contracts that do not hold funds have this emergency function if someone occasionally
      transfers ERC20 tokens directly to this contract
      this metod is callable only by admin, no timelock etc., because this contract is not aimed to hold user funds
*/
function emergencyTransfer(
    address _token,
    address _destination,
    uint256 _amount
) public onlyAdmin {
    if (_token != ETH_TOKEN_ADDRESS) {
        IERC20Upgradeable(_token).safeTransfer(_destination, _amount);
    } else {
        payable(_destination).sendValue(_amount);
    }
    emit EmergencyTransfer(_token, _destination, _amount);
}

Tool Used

Manual Review

Recommendation

Consider keeping track of the collected fees and only allow the yield distributor to withdraw the tracked fee token balances.

GalloDaSballo - M-02 All the arbitrary data is unchecked against user rugging themselves

GalloDaSballo

medium

M-02 All the arbitrary data is unchecked against user rugging themselves

Summary

When using the Synapse Bridge, because of a lack of checks, callers can rug themselves as the data is not validated.

Vulnerability Detail

The contracts can be used to:
-> Swap and transfer to someone else
-> Bridge and transfer to someone else

All these options can be achieved by adding malicious bytes that are not checked in the contract.

In clear contradiction to:

We are assuming that bridge address is stored in trusted contract registry, and thus, even provided
          by capability to pass arbitrary bytes in the call data for the e.g. Synapse bridge, this should
          not provide ability to access or manipulate other users funds in any way.

The synapse bridge code, allows to transfer to another recipient, it also allows to have a fee, which may be set to an irrational value (and hidden in the bytecode unintelligible for an end-user)

See Synapse source code below:
https://etherscan.io/address/0x31fe393815822edacbd81c2262467402199efd0d#code#F15#L247

  function mint(
    address payable to,
    IERC20Mintable token,
    uint256 amount,
    uint256 fee,
    bytes32 kappa
  ) external nonReentrant() whenNotPaused() {

Meaning the callData could be made to transfer to an arbitrary recipient or with a high fee

Impact

Code Snippet

https://github.com/sherlock-audit/2022-10-mover/blob/main/cardtopup_contract/contracts/HardenedTopupProxy.sol#L391-L393

Tool used

Manual Review

Recommendation

Add additional checks to ensure that the recipient is the caller

Bnke0x0 - HardenedTopupProxy's claimFees and emergencyTransfer can become stuck with zero reward transfer

Bnke0x0

medium

HardenedTopupProxy's claimFees and emergencyTransfer can become stuck with zero reward transfer

Summary

There are no checks for the amounts to be transferred via claimFees and emergencyTransfer. As the reward token list is external and an arbitrary token can end up there, when such a token doesn't allow for zero-amount transfers, the reward retrieval can become unavailable.

Vulnerability Detail

Impact

HardenedTopupProxy's claimFees and emergencyTransfer can become stuck with zero reward transfer

Code Snippet

https://github.com/sherlock-audit/2022-10-mover/blob/main/cardtopup_contract/contracts/HardenedTopupProxy.sol#L251

    'IERC20Upgradeable(_token).safeTransfer(msg.sender, _amount);'

https://github.com/sherlock-audit/2022-10-mover/blob/main/cardtopup_contract/contracts/HardenedTopupProxy.sol#L268

 'IERC20Upgradeable(_token).safeTransfer(_destination, _amount);'

Tool used

Manual Review

Recommendation

Consider running the transfers in claimFees and emergencyTransfer only when _amountis positive:

https://github.com/sherlock-audit/2022-10-mover/blob/main/cardtopup_contract/contracts/HardenedTopupProxy.sol#L251

    'if (_amount> 0) {
     IERC20Upgradeable(_token).safeTransfer(msg.sender, _amount);'

https://github.com/sherlock-audit/2022-10-mover/blob/main/cardtopup_contract/contracts/HardenedTopupProxy.sol#L268

 'if (_amount> 0) {
  IERC20Upgradeable(_token).safeTransfer(_destination, _amount);'

cryptphi - Anyone can call setCompleted function in Migrations contract

cryptphi

high

Anyone can call setCompleted function in Migrations contract

Summary

Due to owner state variable being hardcoded to msg.sender , anyone can call Migrations.setCompleted()

Vulnerability Detail

The setCompleted() function in Migrations contract implements a modifier to check that the caller is the owner of the contract. However, due to owner state variable being hardcoded to msg.sender , anyone can call Migrations.setCompleted() and make changes to last_completed_migration

Impact

This is an access control issue.

Code Snippet

https://github.com/sherlock-audit/2022-10-mover/blob/main/cardtopup_contract/contracts/Migrations.sol#L5-L18

State variable owner
address public owner = msg.sender;

Modifier

modifier restricted() {
    require(
      msg.sender == owner,
      "This function is restricted to the contract's owner"
    );
    _;
  }

Restricted function

function setCompleted(uint completed) public restricted {
    last_completed_migration = completed;
  }

Tool used

Manual Review

Recommendation

A more efficient method to initialize the owner state variable is advised. This can be done by using a constructor or a separate function that can change owner state variable.

Bnke0x0 - User's may accidentally overpay inย ย and the excess will be paid to the vault creator

Bnke0x0

medium

User's may accidentally overpay inย ย and the excess will be paid to the vault creator

Summary

It is possible for a user purchasing an option to accidentally overpay

Vulnerability Detail

Impact

User's may accidentally overpay inย ย and the excess will be paid to the vault creator

Code Snippet

https://github.com/sherlock-audit/2022-10-mover/blob/main/cardtopup_contract/contracts/ExchangeProxy.sol#L168

  'require(msg.value >= ethValue, "insufficient ETH provided");'

Tool used

Manual Review

Recommendation

Consider modifying the check such that theย msg.valueย is exactly equal to theย ethValue.

https://github.com/sherlock-audit/2022-10-mover/blob/main/cardtopup_contract/contracts/ExchangeProxy.sol#L168

  'require(msg.value == ethValue, "insufficient ETH provided");'

0xSmartContract - No Storage Gap for Upgradeable Contracts

0xSmartContract

medium

No Storage Gap for Upgradeable Contracts

Summary

For upgradeable contracts, inheriting contracts may introduce new variables. In order to be able to add new variables to the upgradeable contract without causing storage collisions, a storage gap should be added to the upgradeable contract.

If no storage gap is added, when the upgradable contract introduces new variables,
it may override the variables in the inheriting contract.

Vulnerability Detail

Openzeppelin Storage Gaps notification:

Storage Gaps
You may notice that every contract includes a state variable namedย __gap. 
This is empty reserved space in storage that is put in place in Upgradeable contracts. 
It allows us to freely add new state variables in the future without compromising 
the storage compatibility with existing deployments.
It isnโ€™t safe to simply add a state variable because it "shifts down" 
all of the state variables below in the inheritance chain. 
This makes the storage layouts incompatible, as explained inย Writing Upgradeable Contracts. 
The size of theย __gapย array is calculated so that the amount of storage used by a contract 
always adds up to the same number (in this case 50 storage slots).

Impact

Storage gaps are a convention for reserving storage slots in a base contract, allowing future versions of that contract to use up those slots without affecting the storage layout of child contracts.

Code Snippet

To create a storage gap, declare a fixed-size array in the base contract with an initial number of slots.
This can be an array ofย uint256ย so that each element reserves a 32 byte slot. Use the naming conventionย __gapย so that OpenZeppelin Upgrades will recognize the gap:

contract Base {
    uint256 base1;
    uint256[49] __gap;
}

contract Child is Base {
    uint256 child;
}

Tool used

Manual Review

Recommendation

Consider adding a storage gap at the end of the upgradeable contract

uint256[50] private __gap;

Duplicate of #17

Bnke0x0 - NO STORAGE GAP FOR UPGRADEABLE CONTRACTS

Bnke0x0

medium

NO STORAGE GAP FOR UPGRADEABLE CONTRACTS

Summary

For SafeAllowanceResetUpgradeable, which are upgradeable abstract contracts, inheriting contracts may introduce new variables. In order to be able to add new variables to the upgradeable abstract contract without causing storage collisions, a storage gap should be added to the upgradeable abstract contract.

Vulnerability Detail

Impact

If no storage gap is added, when the upgradable abstract contract introduces new variables, it may override the variables in the inheriting contract.

Code Snippet

https://github.com/sherlock-audit/2022-10-mover/blob/main/cardtopup_contract/contracts/utils/SafeAllowanceResetUpgradeable.sol#L9

   'abstract contract SafeAllowanceResetUpgradeable {'

Tool used

Manual Review

Recommendation

Consider adding a storage gap at the end of the upgradeable abstract contract

      'uint256[50] private __gap;'

Duplicate of #17

berndartmueller - The Synapse bridge integration does not validate the low-level call function parameter and can lead to incorrect bridging

berndartmueller

medium

The Synapse bridge integration does not validate the low-level call function parameter and can lead to incorrect bridging

Summary

Allowing arbitrary callData to be passed to the Synapse bridge contract call makes it possible to call any function (accidentally or due to a bug in the frontend) on the target contract. This can lead to incorrect bridging of tokens.

Vulnerability Detail

Tokens can be either bridged via the Synapse bridge or the Across bridge. The Across bridge is called through a well-defined interface. The Synapse bridge, however, allows arbitrary callData to be passed to the call function. In case of accidentally using the wrong Synapse contract functions (e.g. a view-only function), the call will succeed but the tokens will not be bridged. As the necessary top-up events are emitted, the off-chain nodes will consider the bridging to be successful. This will lead to issues with the top-up or accounting and requires manual intervention to fix the issue.

Impact

The Synapse bridge contract can be called with a callData parameter that includes a call to a function which does not bridge tokens, instead, it could be a call to a view-only function (or, if the Synapse bridge contract would have a fallback function, empty calldata would suffice to have a successful call).

The call would succeed, and the transaction succeeds with the necessary CardTopup event emitted, but the tokens will not be bridged. This would then require manual intervention to fix the issue and return token funds to the user.

Code Snippet

HardenedTopupProxy.sol#L421

function _bridgeAssetDirect(uint256 _amount, uint256 _bridgeType, bytes memory _bridgeTxData) internal {
    [...]

    if (_bridgeType == 0)
    {
        // Synapse bridge call data is retrieved by performing a call by the application
        // to bridge SDK and is not transformed by this contract
        bytes memory callData = _bridgeTxData.slice(20, _bridgeTxData.length - 20);
        (bool success, ) = targetAddress.call(callData); // @audit-info `callData` is not validated and can contain any data
        require(success, "BRIDGE_CALL_FAILED");
    } else if (_bridgeType == 1) {

    [...]
}

Tool Used

Manual Review

Recommendation

Consider not allowing arbitrary callData to be passed to the Synapse bridge and instead only allowing the callData to be constructed by the contract itself and provide necessary parameters.

JohnSmith - Overflow on high token amount

JohnSmith

medium

Overflow on high token amount

Summary

A transaction will revert, when amount of transfered tokens is very high

Vulnerability Detail

allowanceTreshold is 1_100_000_000_000_000_000 as set in initialize()

cardtopup_contract/contracts/HardenedTopupProxy.sol
146:     function initialize(uint _chainId, bytes memory _chainIdRLP) public initializer {
...
160:         allowanceTreshold = 1_100_000_000_000_000_000;
161:     }

Cheap tokens may be transfered in high volumes.
When we check that allowance is less than amount + 10% we do a multiplication

cardtopup_contract/contracts/HardenedTopupProxy.sol
288:         require(IERC20Upgradeable(_token).allowance(msg.sender, address(this)) < _amount.mul(allowanceTreshold).div(1e18), "excessive allowance");

_amount.mul(allowanceTreshold) will revert because of overflow, when result is greater than type(uint256).max

Impact

A user cannot topup his card with high amount of tokens.

Code Snippet

https://github.com/sherlock-audit/2022-10-mover/blob/main/cardtopup_contract/contracts/HardenedTopupProxy.sol#L288

Tool used

Manual Review

Recommendation

To reduce chance of overflow we can remind ourselves that _amount * 11 / 10 is also +10%, so one way to fix it will be

cardtopup_contract/contracts/HardenedTopupProxy.sol
146:     function initialize(uint _chainId, bytes memory _chainIdRLP) public initializer {
...
- 160:         allowanceTreshold = 1_100_000_000_000_000_000;
+ 160:         allowanceTreshold = 11;
cardtopup_contract/contracts/HardenedTopupProxy.sol
- 288:         require(IERC20Upgradeable(_token).allowance(msg.sender, address(this)) < _amount.mul(allowanceTreshold).div(1e18), "excessive allowance");
+ 288:         require(IERC20Upgradeable(_token).allowance(msg.sender, address(this)) < _amount.mul(allowanceTreshold).div(10), "excessive allowance");

or write more complex check using try catch or unchecked{} math

Duplicate of #6

8olidity - Not calling approve(0) before setting a new approval causes the call to revert when used with Tether (USDT)

8olidity

medium

Not calling approve(0) before setting a new approval causes the call to revert when used with Tether (USDT)

Summary

Not calling approve(0) before setting a new approval causes the call to revert when used with Tether (USDT)

Vulnerability Detail

Not calling approve(0) before setting a new approval causes the call to revert when used with Tether (USDT)

Impact

Some tokens (like USDT) do not work when changing the allowance from an existing non-zero allowance value (it will revert if the current approval is not zero to protect against front-running changes of approvals). These tokens must first be approved for zero and then the actual allowance can be approved.

Code Snippet

https://github.com/sherlock-audit/2022-10-mover/blob/main/cardtopup_contract/contracts/utils/SafeAllowanceReset.sol#L20

  function resetAllowanceIfNeeded(IERC20 _token, address _spender, uint256 _amount) internal {
    uint256 allowance = _token.allowance(address(this), _spender);
    if (allowance < _amount) {
      uint256 newAllowance = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff;
      IERC20(_token).safeIncreaseAllowance(address(_spender), newAllowance.sub(allowance));
    }
  }

Tool used

Manual Review

Recommendation

Use approve(address(_spender), 0) to set the allowance to zero immediately before each of the existing approve() calls.

Duplicate of #58

seyni - `ExchangeProxy.executeSwap` is only callable by `transferProxyAddress`

seyni

medium

ExchangeProxy.executeSwap is only callable by transferProxyAddress

Summary

ExchangeProxy.executeSwap should be callable by anyone, but a check in ExchangeProxy.executeSwapDirect make it only callable by transferProxyAddress.

Vulnerability Detail

ExchangeProxy.executeSwap should be callable by anyone, the function send ERC20 tokens from msg.sender to the ExchangeProxy contract, then call ExchangeProxy.executeSwapDirect with msg.sender still being tx.origin but in ExchangeProxy.executeSwapDirect the following check is done:

        require(msg.sender == transferProxyAddress, "transfer proxy only");

Therefore, no one but transferProxyAddress can use ExchangeProxy.executeSwap .

Impact

ExchangeProxy.executeSwap should be callable by anyone, but is only callable by transferProxyAddress.

Code Snippet

https://github.com/sherlock-audit/2022-10-mover/blob/main/cardtopup_contract/contracts/ExchangeProxy.sol#L102

        return executeSwapDirect(msg.sender, _tokenFrom, _tokenTo, _amount, 0, _data);

https://github.com/sherlock-audit/2022-10-mover/blob/main/cardtopup_contract/contracts/ExchangeProxy.sol#L139

        require(msg.sender == transferProxyAddress, "transfer proxy only");

Tool used

Manual Review

Recommendation

A boolean could become true if the call come from executeSwap and could be added in the require statement:

        require(msg.sender == transferProxyAddress || executeSwapBool, "transfer proxy only");

Then, it would be reinitialized to false after the check is done.

cryptphi - Anyone can call setCompleted function in Migrations contract.

cryptphi

high

Anyone can call setCompleted function in Migrations contract.

Summary

Due to owner state variable being hardcoded to msg.sender , anyone can call Migrations.setCompleted()

Vulnerability Detail

The setCompleted() function in Migrations contract implements a modifier to check that the caller is the owner of the contract. However, due to owner state variable being hardcoded to msg.sender , anyone can call Migrations.setCompleted() and make changes to last_completed_migration

Impact

This is an access control issue.

Code Snippet

https://github.com/sherlock-audit/2022-10-mover/blob/main/cardtopup_contract/contracts/Migrations.sol#L5-L18

State variable owner
address public owner = msg.sender;

Modifier

modifier restricted() {
    require(
      msg.sender == owner,
      "This function is restricted to the contract's owner"
    );
    _;
  }

Restricted function

function setCompleted(uint completed) public restricted {
    last_completed_migration = completed;
  }

Tool used

Manual Review

Recommendation

A more efficient method to initialize the owner state variable is advised. This can be done by using a constructor or a separate function that can change owner state variable.

berndartmueller - Collected fees can be used by anyone to top-up

berndartmueller

high

Collected fees can be used by anyone to top-up

Summary

Anyone can use collected fees by the ExchangeProxy contract to top-up by providing arbitrary call data to the swap contract.

Vulnerability Detail

The ExchangeProxy contract collects swap fees and keeps them in escrow for later withdrawal by the yield distributor. However, an attacker is able to provide arbitrary _convertData in the HardenedTopupProxy.CardTopupPermit function. This parameter is used and passed on to the ExchangeProxy.executeSwapDirect function without validation. Then it is used to call the executorAddress to do the token swap. Hence, it's possible to instruct the swap contract to swap the collected fees from the ExchangeProxy contract instead of only swapping the tokens provided by the user (the spending allowance is set to the maximum allowance before in line 156)

Impact

An attacker can use the residual token balances (i.e. collected fees) from the ExchangeProxy contract as the amount for the top-up.

Code Snippet

To demonstrate this issue, use the provided test case can use fees from exchange proxy for topup in https://gist.github.com/berndartmueller/5cfa9d784a32ecba92eb6abaf4d464d9. Copy the test file into test/ExploitTopup.test.js and run truffle test. It demonstrates how an attacker can use 10 DAI to receive a top-up worth 900e6 USDC (collected fees).

ExchangeProxy.sol#L174

function executeSwapDirect(
    address _beneficiary,
    address _tokenFrom,
    address _tokenTo,
    uint256 _amount,
    uint256 _exchangeFee,
    bytes memory _data
) public payable override returns (uint256) {
    require(msg.sender == transferProxyAddress, "transfer proxy only");

    // extract values from bytes array provided
    address executorAddress;
    address spenderAddress;
    uint256 ethValue;

    bytes memory callData = ByteUtil.slice(_data, 72, _data.length - 72);
    assembly {
        executorAddress := mload(add(_data, add(0x14, 0)))
        spenderAddress := mload(add(_data, add(0x14, 0x14)))
        ethValue := mload(add(_data, add(0x20, 0x28)))
    }

    // allow spender to transfer tokens from this contract
    if (_tokenFrom != ETH_TOKEN_ADDRESS && spenderAddress != address(0)) {
        require(trustedRegistryContract.isWhitelisted(spenderAddress), "allowance to non-trusted");
        resetAllowanceIfNeeded(IERC20(_tokenFrom), spenderAddress, _amount);
    }

    // remember the actual balance of target token (fees could reside on this contract balance)
    uint256 balanceBefore = 0;
    if (_tokenTo != ETH_TOKEN_ADDRESS) {
        balanceBefore = IERC20(_tokenTo).balanceOf(address(this));
    } else {
        balanceBefore = address(this).balance;
    }

    // regardless of stated amount, the ETH value passed to exchange call must be provided to the contract
    require(msg.value >= ethValue, "insufficient ETH provided");

    // don't allow to call non-trusted addresses
    require(trustedRegistryContract.isWhitelisted(executorAddress), "call to non-trusted");

    // ensure no state passed, no reentrancy, etc.
    (bool success, ) = executorAddress.call{value: ethValue}(callData); // @audit-info `callData` can contain any arbitrary high swap amount
    require(success, "SWAP_CALL_FAILED");

    // always rely only on actual amount received regardless of called parameters
    uint256 amountReceived = 0;
    if (_tokenTo != ETH_TOKEN_ADDRESS) {
        amountReceived = IERC20(_tokenTo).balanceOf(address(this));
    } else {
        amountReceived = address(this).balance;
    }
    amountReceived = amountReceived.sub(balanceBefore);

    require(amountReceived > 0, "zero amount received");

    // process exchange fee if present (in deposit we get pool tokens, so process fees after swap, here we take fees in source token)
    // fees are left on this contract address and are harvested by yield distributor
    //uint256 feeAmount = amountReceived.mul(_exchangeFee).div(1e18);
    amountReceived = amountReceived.sub(
        amountReceived.mul(_exchangeFee).div(1e18)
    ); // this is return value that should reflect actual result of swap (for deposit, etc.)

    if (_tokenTo != ETH_TOKEN_ADDRESS) {
        //send received tokens to beneficiary directly
        IERC20(_tokenTo).safeTransfer(_beneficiary, amountReceived);
    } else {
        //send received eth to beneficiary directly
        payable(_beneficiary).sendValue(amountReceived);
        // payable(_beneficiary).transfer(amountReceived);
        // should work for external wallets (currently is the case)
        // but wont work for some other smart contracts due to gas stipend limit
    }

    emit ExecuteSwap(_beneficiary, _tokenFrom, _tokenTo, _amount, amountReceived);

    // amount received is used to check minimal amount condition set by calling app from topup proxy contract
    return amountReceived;
}

Tool Used

Manual Review

Recommendation

Consider validating the spent amount of the ERC-20 token _tokenFrom to equal the desired swap _amount in the ExchangeProxy.executeSwapDirect function. This prevents swapping more than the provided _amount function parameter.

Duplicate of #112

csanuragjain - Fee on transfer token not considered

csanuragjain

medium

Fee on transfer token not considered

Summary

It was observed that while processing topup, contract is not considering token with fee on transfer. if cardTopupToken is set as a token with fees then the swaping will fail from existing contract balance

Vulnerability Detail

  1. Observe the _processTopup function
function _processTopup(address _beneficiary, address _token, uint256 _amount, uint256 _expectedMinimumReceived, bytes memory _convertData, uint256 _bridgeType, bytes memory _bridgeTxData, bytes32 _receiverHash) internal
    {
        // don't go further is contract function is paused (by admin or pauser)
        require(paused == false, "operations paused");

        if (_token == cardTopupToken) {
            // beneficiary is msg.sender (perform static check)
            IERC20Upgradeable(_token).safeTransferFrom(_beneficiary, address(this), _amount);

            uint256 feeAmount = _amount.mul(topupFee).div(1e18);

            // bridge from _beneficiary to card L1 relay
            _bridgeAssetDirect(_amount.sub(feeAmount), _bridgeType, _bridgeTxData);

            emit CardTopup(_beneficiary, _token, _amount, _amount.sub(feeAmount), _receiverHash);
            return;
        }
...
  1. Now if token is cardTopupToken which takes fee on transfer then contract would have never received the _amount-fees
if (_token == cardTopupToken) {
            // beneficiary is msg.sender (perform static check)
            IERC20Upgradeable(_token).safeTransferFrom(_beneficiary, address(this), _amount);

            uint256 feeAmount = _amount.mul(topupFee).div(1e18);
  1. This means _bridgeAssetDirect will use the deducted fees part from own contract balance
_bridgeAssetDirect(_amount.sub(feeAmount), _bridgeType, _bridgeTxData);

Impact

Loss of funds

Code Snippet

https://github.com/sherlock-audit/2022-10-mover/blob/main/cardtopup_contract/contracts/HardenedTopupProxy.sol#L322

Tool used

Manual Review

Recommendation

Calculate the balance before and post transfer, the difference will tell the exact amount transferred to contract

Duplicate of #39

caventa - TokenFrom and TokenTo should not be the same

caventa

medium

TokenFrom and TokenTo should not be the same

Summary

TokenFrom and TokenTo should not be the same.

Vulnerability Detail

Swapping should be executed only when _tokenFrom and _tokenTo are not the same.
(See ExchangeProxy.sol#L92-L93 and ExchangeProxy.sol#L133-L134)

Impact

It is good to add the validation in ExchangeProxy.sol rather than relying on the external callData function (See ExchangeProxy.sol#L174) to do the same validation.

Code Snippet

https://github.com/sherlock-audit/2022-10-mover/blob/main/cardtopup_contract/contracts/ExchangeProxy.sol#L92-L93

https://github.com/sherlock-audit/2022-10-mover/blob/main/cardtopup_contract/contracts/ExchangeProxy.sol#L133-L134

https://github.com/sherlock-audit/2022-10-mover/blob/main/cardtopup_contract/contracts/ExchangeProxy.sol#L174

https://github.com/sherlock-audit/2022-10-mover/blob/main/cardtopup_contract/contracts/ExchangeProxy.sol#L139

Tool used

Manual Review

Recommendation

Add require(_tokenFrom != _tokenTo, "token from and token to should not be the same"); to the line before ExchangeProxy.sol#L139

berndartmueller - Protocol does not work with fee-on-transfer tokens

berndartmueller

medium

Protocol does not work with fee-on-transfer tokens

Summary

Fee-on-transfer tokens are not supported by the protocol. This leads to underfunded token transfers and the transactions to revert when swapping/bridging, preventing top-ups with fee-on transfer tokens.

Vulnerability Detail

ERC20 tokens may make specific customizations to their ERC20 contracts.
One type of these tokens is deflationary tokens that charge a specific fee for every transfer() or transferFrom().

In the current protocol implementation, both the ExchangeProxy and the HardenedTopupProxy contracts assume that the received amount is the same as the transfer amount, and use it for internal balance bookkeeping.

However, if the token is a fee-on transfer token, the actually received amount can differ and will be less than the initial transfer amount. This will cause the transaction to revert due to insufficient tokens available for bridging/swapping.

Impact

The transaction will revert when trying to bridge or swap tokens that are fee-on transfer tokens, preventing the use of fee-on transfer tokens for top-ups.

Code Snippet

ExchangeProxy.sol#L99

function executeSwap(
  address _tokenFrom,
  address _tokenTo,
  uint256 _amount,
  bytes memory _data
) public payable override returns (uint256) {
  // native token doesn't need to be transferred explicitly, it's in tx.value
  if (_tokenFrom != ETH_TOKEN_ADDRESS) {
      IERC20(_tokenFrom).safeTransferFrom(msg.sender, address(this), _amount);
  }
  // after token is transferred to this contract, call actual swap
  return executeSwapDirect(msg.sender, _tokenFrom, _tokenTo, _amount, 0, _data);
}

HardenedTopupProxy.sol#L315-L343

function _processTopup(address _beneficiary, address _token, uint256 _amount, uint256 _expectedMinimumReceived, bytes memory _convertData, uint256 _bridgeType, bytes memory _bridgeTxData, bytes32 _receiverHash) internal
{
  // don't go further is contract function is paused (by admin or pauser)
  require(paused == false, "operations paused");

  // if execution passes to here, it means:
  // 1. operations are active;
  // 2. allowance check is passed, allowance is set and we can use funds from _beneficiary's address;
  // next steps are:
  // 1. transfer token to this contract or exchange proxy
  //    TODO (future version): check if token must be unwrapped by a partner
  //    (vault tokens, etc. that could not be swapped and should be unwrapped in some way)
  // 2. swap if needed and transfer USDC to this contract
  // 3. deduct topup fee (if needed)
  // 4. check allowance to the bridge contract
  // 5. check the bridge address is in the whitelist and perform a call to bridge

  // if the token to be provided matches the defined cardTopupToken, do not perform any
  // swap, just deduct topup fee (if fee > 0) and proceed to bridging for settlement
  if (_token == cardTopupToken) {
      // beneficiary is msg.sender (perform static check)
      IERC20Upgradeable(_token).safeTransferFrom(_beneficiary, address(this), _amount); // @audit-info `_amount` is transferred

      uint256 feeAmount = _amount.mul(topupFee).div(1e18); // @audit-info `_amount` received could be less than anticipated for fee-on transfer tokens

      // bridge from _beneficiary to card L1 relay
      _bridgeAssetDirect(_amount.sub(feeAmount), _bridgeType, _bridgeTxData);

      emit CardTopup(_beneficiary, _token, _amount, _amount.sub(feeAmount), _receiverHash);
      return;
  }

  // conversion is required, perform swap through exchangeProxy
  if (_token != ETH_TOKEN_ADDRESS) {
      // transfer tokens on the exchange proxy balance before performing swap call
      IERC20Upgradeable(_token).safeTransferFrom(_beneficiary, address(exchangeProxyContract), _amount); // @audit-info `_amount` is transferred
  }

  // exchange proxy is trusted and would check swap provider on its own in trusted registry contract
  uint256 amountReceived =
      IExchangeProxy(address(exchangeProxyContract)).executeSwapDirect{value: msg.value}(
          address(this),
          _token,
          cardTopupToken,
          _amount, // @audit-info `_amount` received could be less than anticipated for fee-on transfer tokens
          exchangeFee,
          _convertData
      );

  [..]
}

Tool Used

Manual Review

Recommendation

Consider using the actually received amount by calculating the difference in token balance before and after the token transfer.

8olidity - Maxamount and minamount are not checked and may cause DOS

8olidity

medium

Maxamount and minamount are not checked and may cause DOS

Summary

Maxamount and minamount are not checked and may cause DOS

Vulnerability Detail

Maxamount and minamount are not checked and may cause DOS

Impact

minAmount and maxAmount can be set arbitrarily, with no time limit or size limit.

    function setMinAmount(uint256 _minAmount) public onlyAdmin {
        minAmount = _minAmount;
    }

    function setMaxAmount(uint256 _maxAmount) public onlyAdmin {
        maxAmount = _maxAmount;
    }

It is possible to set the result to minAmount > maxAmount. If this happens, CardTopupMPTProof(),CardTopupTrusted() and CardTopupPermit() will not work properly because they all have the check _amount function

require(_amount >= minAmount, "minimum amount not met");
require(_amount < maxAmount, "maximum amount exceeded");

Code Snippet

https://github.com/sherlock-audit/2022-10-mover/blob/main/cardtopup_contract/contracts/HardenedTopupProxy.sol#L218-L224

Tool used

Manual Review

Recommendation

Verify the relationship between minamount and maxamount

Duplicate of #123

0xSmartContract - Missing ReEntrancy Guard to `executeSwapDirect` function

0xSmartContract

high

Missing ReEntrancy Guard to executeSwapDirect function

Summary

There is no re-entry risk on true ERC-20 tokens that work according to the spec (i.e. audited, etc.).

However you can write a malicious ERC-20 with custom safetransferFrom() or approve() that have re-entrancy hooks to attack a target.

Furthermore ERC-777 is backwards compatible token standard with ERC-20 standard. ERC-777 has better usability, but it has transfer hooks that can cause re-entrancy.

Vulnerability Detail

openzeppelin's view on the reentrancy problem

ERC20 generally doesn't result in reentrancy, however ERC777 tokens can and they can maskerade as ERC20. So if a contract interacts with unknown ERC20 tokens it is better to be safe and consider that transfers can create reentrancy problems.

Impact

Although reentrancy attack is considered quite old over the past two years, there have been cases such as:

  • Uniswap/LendfMe hacks (2020) ($25 mln, attacked by a hacker using a reentrancy)

  • The BurgerSwap hack (May 2021) ( $7.2 million because of a fake token contract and a reentrancy exploit.)

  • The SURGEBNB hack (August 2021) ($4 million seems to be a reentrancy-based price manipulation attack.)

  • CREAM FINANCE hack (August 2021) ($18.8 million, reentrancy vulnerability allowed the exploiter for the second borrow.)

  • Siren protocol hack (September 2021) ($3.5 million, AMM pools were exploited through reentrancy attack.)

Type of Reentrancy

Details
1 - Single Function Reentrancy
2 - Cross-Function Reentrancy
3 - Cross-Contract Reentrancy

Code Snippet

Must be re-entrancy guard to below functions

cardtopup_contract/contracts/ExchangeProxy.sol:
   90       */
   91:     function executeSwap(
   92:         address _tokenFrom,
   93:         address _tokenTo,
   94:         uint256 _amount,
   95:         bytes memory _data
   96:     ) public payable override returns (uint256) {
   97:         // native token doesn't need to be transferred explicitly, it's in tx.value
   98:         if (_tokenFrom != ETH_TOKEN_ADDRESS) {
   99:             IERC20(_tokenFrom).safeTransferFrom(msg.sender, address(this), _amount);
  100:         }
  101:         // after token is transferred to this contract, call actual swap
  102:         return executeSwapDirect(msg.sender, _tokenFrom, _tokenTo, _amount, 0, _data);
  103:     }



cardtopup_contract/contracts/ExchangeProxy.sol:
  194  
  195:         if (_tokenTo != ETH_TOKEN_ADDRESS) {
  196:             //send received tokens to beneficiary directly
  197:             IERC20(_tokenTo).safeTransfer(_beneficiary, amountReceived);
  198:         } else {
  199:             //send received eth to beneficiary directly
  200:             payable(_beneficiary).sendValue(amountReceived);
  201:             // payable(_beneficiary).transfer(amountReceived);
  202:             // should work for external wallets (currently is the case)
  203:             // but wont work for some other smart contracts due to gas stipend limit
  204          }

Duplicate of #119

Tool used

Manual Review

Recommendation

Use Openzeppelin or Solmate Re-Entrancy pattern

Here is a example of a re-entracy guard

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract ReEntrancyGuard {
    bool internal locked;

    modifier noReentrant() {
        require(!locked, "No re-entrancy");
        locked = true;
        _;
        locked = false;
    }
}

GalloDaSballo - L-02 Frontrun of initializers

GalloDaSballo

low

L-02 Frontrun of initializers

Summary

Initializer can be front-run setting the caller to the owner.

Vulnerability Detail

https://github.com/sherlock-audit/2022-10-mover/blob/main/cardtopup_contract/contracts/ContractWhitelist.sol#L25-L26

    function initialize() public initializer {

Impact

Code Snippet

Tool used

Manual Review

Recommendation

It may be worth creating a constructor to set the only caller for initialize, or simply make sure you're the first one calling it

JohnSmith - `HardenedTopupProxy` and `ExchangeProxycan` can become stuck because of one step approval for some tokens

JohnSmith

medium

HardenedTopupProxy and ExchangeProxycan can become stuck because of one step approval for some tokens

Summary

Some tokens do not allow for approval of positive amount when allowance is positive already (to handle approval race condition, most known example is USDT on Ethereum).
They revert when we try to approve a value m if it already has approved some value n > 0.

Vulnerability Detail

Some ERC20 forbid the approval of positive amount when the allowance is positive:
https://github.com/d-xo/weird-erc20#approval-race-protections
HardenedTopupProxy and ExchangeProxycan have a functionality at some point to reset the allowance of a particular token.

cardtopup_contract/contracts/utils/SafeAllowanceResetUpgradeable.sol
and
cardtopup_contract/contracts/utils/SafeAllowanceReset.sol

20:   function resetAllowanceIfNeeded(IERC20Upgradeable _token, address _spender, uint256 _amount) internal {
21:     uint256 allowance = _token.allowance(address(this), _spender);
22:     if (allowance < _amount) {
23:       uint256 newAllowance = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff;
24:       IERC20Upgradeable(_token).safeIncreaseAllowance(address(_spender), newAllowance.sub(allowance));
25:     }
26:   }

When time comes( if (allowance < _amount)) the function will revert, because it will try to approve new allowance when old one is set and greater than 0.

Impact

ExchangeProxy will fail to do swaps

cardtopup_contract/contracts/ExchangeProxy.sol
156:             resetAllowanceIfNeeded(IERC20(_tokenFrom), spenderAddress, _amount);

fail to reset yieldDistributorAddress for ExchangeProxy and HardenedTopupProxy, i.e. when you want to reset the allowance or switch to previous one

cardtopup_contract/contracts/ExchangeProxy.sol
224:     function setYieldDistributor(address _tokenAddress, address _distributorAddress) public onlyAdmin {
...
228:         resetAllowanceIfNeeded(IERC20(_tokenAddress), _distributorAddress, ALLOWANCE_SIZE);

also need to double check when you decide to support some others tokens for card topup

cardtopup_contract/contracts/HardenedTopupProxy.sol
414:         resetAllowanceIfNeeded(IERC20Upgradeable(cardTopupToken), targetAddress, _amount);

Code Snippet

https://github.com/sherlock-audit/2022-10-mover/blob/main/cardtopup_contract/contracts/utils/SafeAllowanceResetUpgradeable.sol#L20-L26
https://github.com/sherlock-audit/2022-10-mover/blob/main/cardtopup_contract/contracts/utils/SafeAllowanceReset.sol#L20-L26

Tool used

Manual Review

Recommendation

-       uint256 newAllowance = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff;
-       IERC20(_token).safeIncreaseAllowance(address(_spender), newAllowance.sub(allowance));
+       IERC20(_token).safeApprove(address(_spender), 0);
+       IERC20(_token).safeApprove(address(_spender), type(uint256).max);

Duplicate of #58

8olidity - executeSwap() locks out user ETH

8olidity

medium

executeSwap() locks out user ETH

Summary

executeSwap() locks out user ETH

Vulnerability Detail

If both tokenFrom and tokenTo are not ETH, then the contract return fee will be an asset of ERC20(_tokenTo),

        if (_tokenTo != ETH_TOKEN_ADDRESS) {
            //send received tokens to beneficiary directly
            IERC20(_tokenTo).safeTransfer(_beneficiary, amountReceived);
        } else {
            //send received eth to beneficiary directly
            payable(_beneficiary).sendValue(amountReceived);
            // payable(_beneficiary).transfer(amountReceived);
            // should work for external wallets (currently is the case)
            // but wont work for some other smart contracts due to gas stipend limit
        }

But if the user sends ETH to the contract during the operation, for this reason, even if ethValue is 0

require(msg.value >= ethValue, "insufficient ETH provided");

We still don't rule out the possibility that users will send money. If so, the ETH sent by the user is locked in the contract and cannot be taken out by the user. Because the contract doesn't allow for that.

Impact

executeSwap() locks out user ETH

Code Snippet

https://github.com/sherlock-audit/2022-10-mover/blob/main/cardtopup_contract/contracts/ExchangeProxy.sol#L131-L210

Tool used

Manual Review

Recommendation

Consider the case where neither from nor to is ETH, but ETH is sent

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.