2022-10-mover-judging's People
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
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
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
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
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);
}
}
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;
}
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
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
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));
}
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
'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
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
- 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)
);
...
}
- 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
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
(bool success, ) = executorAddress.call{value: ethValue}(callData);
(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 forcurrentBlock > 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 ifcardPartnerAddress
isn't initialized. HardenedTopupProxy.CardTopupTrusted
would be unusable ifallowanceSignatureTimespan
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
require(block.timestamp - _timestamp < allowanceSignatureTimespan, "old sig");
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
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
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
- 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");
}
- 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
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
- Observe the executeSwapDirect function
- 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
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
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
- 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
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
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
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
Tool used
Manual Review
Recommendation
Add additional checks to ensure that the recipient is the caller
GalloDaSballo - L-01 Extra value will be lost
GalloDaSballo
low
L-01 Extra value will be lost
Summary
require(msg.value >= ethValue, "insufficient ETH provided");
Best to check for exact matching to avoid needless costs due to user mistakes
Vulnerability Detail
Impact
Code Snippet
Tool used
Manual Review
Recommendation
Change the >=
to an ==
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
'IERC20Upgradeable(_token).safeTransfer(msg.sender, _amount);'
'IERC20Upgradeable(_token).safeTransfer(_destination, _amount);'
Tool used
Manual Review
Recommendation
Consider running the transfers in claimFees and emergencyTransfer only when _amountis positive:
'if (_amount> 0) {
IERC20Upgradeable(_token).safeTransfer(msg.sender, _amount);'
'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
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
'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.
'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
'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
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
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
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
return executeSwapDirect(msg.sender, _tokenFrom, _tokenTo, _amount, 0, _data);
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
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).
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
- 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;
}
...
- 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);
- 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
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
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
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
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
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
Tool used
Manual Review
Recommendation
Consider the case where neither from nor to is ETH, but ETH is sent
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
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.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google โค๏ธ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.