Loading...
publicMint
is an payable function (i.e receive ether to the contract). But there is no way to take out funds from the contract leads to permanent locking of funds.
function publicMint(uint256 amount) external payable onlyNotBlacklisted{
...
}
Add a function that allows owner to transfer funds from contract.
function claimFunds(address _to) payable external onlyOwner {
require(_to != address(0), "can't transfer to address(0)");
uint256 amt = address(this).balance;
(bool success, )payable(_to).call{value: amt}("");
require(success, "funds transfer failed");
emit FundsClaimed(_to, amt);
}
claimInterest
violates CEI pattern makes it vulnerable for reentrancy for malicious tokensclaimInterest
function transfers interest amt calculated from getAccruedInterest
which calculates interest based on difference between timestamps.
function getAccruedInterest(address user) public view returns (uint256) {
stakeInfo memory t = stakes[user];
if(block.timestamp - t.timestamp >= 30 days){
return t.amt/2;
}
else if(block.timestamp - t.timestamp >= 7 days){
return t.amt/10;
}
else if(block.timestamp - t.timestamp >= 1 days){
return t.amt/100;
}
return 0;
}
Interest calculation is dependent on stakes[user]
but the claimInterest transfers token first and then updates the timestamp.
function claimInterest() public {
...
stakingToken.transfer(msg.sender,inrest); // Interaction
stakes[msg.sender].timestamp = block.timestamp; // Effects
}
If the staking token is ERC777 or any malicious token. It leads to reentrancy in claimInterest function which can drain the contract balance.
Add a function that allows owner to transfer funds from contract.
function claimInterest() public {
...
stakes[msg.sender].timestamp = block.timestamp; // Effects
stakingToken.transfer(msg.sender,inrest); // Interaction
}
transferFrom
may silently fail leads inconsistency in the state of contractStake function uses transferFrom
to transfer tokens from sender to the contract. But it may fail silently with out revert which leads to inconsistency in state variables totalStaked
, stakes
.
function stake(uint256 amount) external {
...
// @audit transfer from may silently fail
stakingToken.transferFrom(msg.sender,address(this),amount);
totalStaked += amount;
t.amt = amount;
stakes[msg.sender] = t;
}
transferFrom returns a boolean value. Add a check for successful transaction.
function stake(uint256 amount) external {
...
bool success = stakingToken.transferFrom(msg.sender,address(this),amount);
require(success, "transferring tokens failed");
...
}
Note: Excluded frontrunning/backrunning/sandwich attacks that are basically overwhelming to think about during the contest.
In both of the removeLiquidity
and tokenToEthSwap
functions, transfer() is used for native ETH withdrawal. The transfer()
and send()
functions forward a fixed amount of 2300 gas. Historically, it has often been recommended to use these functions for value transfers to guard against reentrancy attacks. However, the gas cost of EVM instructions may change significantly during hard forks which may break already deployed contract systems that make fixed assumptions about gas costs. For example. EIP 1884 broke several existing smart contracts due to a cost increase of the SLOAD instruction.
The use of the deprecated transfer() function for an address will inevitably make the transaction fail when:
Use call() instead of transfer().
payable(msg.sender).call{value: ethAmt}("");
ETH fees accumulated from tokenToEthSwap()
operation are permanently frozen within the contract as there is no way designed to retrieve them.
A permanent freeze of ETH revenue of the project.
In addition to below recommendation, ensure owner doesn't pull excess ether other than collected fee.
/// @dev used for rescuing swap fees paid to the contract in ETH
function rescueETH(address destination) external payable onlyOwner {
(bool sent, ) = destination.call{value: feevalue}('');
require(sent, 'failed');
}
transferFrom
may silently failStake function uses transferFrom
to transfer tokens from sender to the contract. But it may fail silently with out revert.
transferFrom returns a boolean value. Implement a check for successful transaction.
bool success = stakingToken.transferFrom(msg.sender,address(this),amount);
require(success, "transferring tokens failed");
removeLiquidity
violates CEI pattern leads to read-only reenrtancyImplementation of removeLiquidity
does burning of LPTokens after transferring the tokens and ether to the caller.
function removeLiquidity(uint256 amountOfLPTokens) external {
...
token.transfer(msg.sender,v4);
payable(msg.sender).transfer(v5);
_burn(msg.sender, amountOfLPTokens);
...
}
As stated in Exchange::Inf-01
if the code is modified to use call
instead of transfer
, it allows caller contract to make a call to other functions.
Attack Scenario:
removeLiquidity
to remove 10^18 lp tokens._burn
is being called after external call. The state of totalSupply()
isn't modified.addLiquidity
in fallback
uint y = (totalSupply() * msg.value) /(ethBalance-msg.value);
_mint(msg.sender,y);
As totalSupply() is higher value, attacker receives more lp tokens than expected.
burn LP tokens prior external calls
function removeLiquidity(uint256 amountOfLPTokens) external {
...
// burn lp tokens
_burn(msg.sender, amountOfLPTokens);
// then transfer ERC20 and Ether
token.transfer(msg.sender,v4 );
payable(msg.sender).transfer(v5);
}
Thankyou everyone participated in contest 10 and a special mention and congrats to NitinSinha 🥇
& 🤓 Get ready for the upcoming contest!