I have a solidity code to audit like this
pragma solidity ^0.8.0;
import "#openzeppelin/contracts/token/ERC20/IERC20.sol";
// Allow to split the balance through complex rules
interface Split{
function getAddressAndAmountToSplit() view external returns(address, uint);
}
// MyBank contract
// This contract allows anyone to store any ERC20 tokens
contract MyBank {
// (token => user => amount)
mapping (address => mapping(address => uint)) public userBalance;
// (address => Split contract)
mapping (address => Split) splits;
// Deposit ERC20 tokens to the contracts
// The user must approve the bank before calling addToBalance
function addToBalance(IERC20 token, uint amount) external {
token.transferFrom(msg.sender, address(this), amount);
userBalance[address(token)][msg.sender] += amount;
}
// Withdraw part of the balance
function withdrawBalance(IERC20 token) external {
token.transfer(msg.sender, userBalance[address(token)][msg.sender]);
userBalance[address(token)][msg.sender] = 0;
}
// Allow to register a split contract
function registerSplit(Split split) external {
splits[msg.sender] = split;
}
// Split the balance into two accounts
// The usage of a Split contract allows to create complex split strategies
function splitBalance(IERC20 token) external {
Split split = splits[msg.sender];
require(split != Split(address(0x0)));
uint balance = userBalance[address(token)][msg.sender];
(address dest, uint amount) = Split(split).getAddressAndAmountToSplit();
userBalance[address(token)][dest] = amount;
userBalance[address(token)][msg.sender] = balance - amount;
}
}
What I found.
function withdrawBalance(IERC20 token) external possible reentrancy attack, because we check balance in the end
function splitBalance(IERC20 token) external - vulnerable business logic, because if amount is greater than balance we get negative value and possible integer overflow
If you have any idea of possible vulnerabilities of code above, please feel free to provide any further assistance
Probably a bit late but if anyone reads this for why I think these aren’t vulnerabilities. First of all, there are no reentrancy attacks possible here, due to the fact that transfer function only forwards 2300 gas, which is quite not enough to execute something meaningful.
Secondly, its possible to add a require check if balance is bigger than the amount. But since contract is using later than or equal to 0.8.0 compiler versions, if amount is indeed bigger than balance, it will automatically revert due to underflow.
I think as a smart contract auditor, you should know these better.
Related
e.g; With the receive() function in the BEP20 network, I capture when a money is transferred and trade with a script. But when other tokens such as WBNB, USDT are sent, I cannot do anything.
What I want to do: convert all coins and tokens directly transferred to the contract to busd via receive or fallback (or whichever works with) pancakeswap and mapping(address => uint) balance; I want to import the MAP into it.
I searched a lot but couldn't find the result I was looking for.
Could you please share which is the required source code for this process?
I using this function:
contract SendMoney{
mapping(address => uint) balance;
receive() external payable {
SendedMoney(msg.sender, msg.value);
}
function SendedMoney(address _senderaddress, uint _amount){
balance[_senderaddress] = _amount;
}
}
Those are BEP20 Tokens they don't have a receive() function.
This means the smart contract doesn't know that somebody sent you those tokens.
You would have to implement some off-chain bot that would track if your contract got any tokens and then call some swap() function.
I have a ERC20 smart contract with edited transfer function
function transfer(address recipient, uint256 amount) public virtual override returns (bool) {
if(_transactionMaxValue > 0){
require(_transactionMaxValue >= amount, "You can not transfer more than 1000000 tokens at once!");
}
transfer(recipient, amount);
}
I have added if statement in transfer function, in case user indicates limit per single transaction before deploying. I was wondering if this would affect gas fees when transferring tokens, in order to decide weather to leave the "universal" ERC20 smart contract template with indictable transaction limit or compose new one, with no if statement, if no transaction limit was indicated.
I was wondering if this would affect gas fees when transferring tokens
Adding the (if and require) conditions increases the total amount of gas used, but the increase is small in the context of gas usage of other operations of the parent transfer() function. It's because in the overriding function, you're performing "just" one more storage read and few other operations in memory.
Execution of your transfer() function costs 54,620 gas (including the parent function; assuming the require() condition doesn't fail). While execution of just the parent transfer() function costs 52,320 gas.
You need to use super.transfer(recipient, amount); if you want to invoke the parent transfer() function. Without the super keyword, you'd lock the script in an infinite recursion, always invoking itself.
Also, in order to correctly override the parent function, you need to state the override modifier (and virtual if you are planning to override this function as well) before the public visibility modifier, and to return the value as declared.
pragma solidity ^0.8.0;
import "#openzeppelin/contracts/token/ERC20/ERC20.sol";
contract MyToken is ERC20 {
uint256 _transactionMaxValue = 1000000;
constructor() ERC20("MyToken", "MyT") {
_mint(msg.sender, 1000000);
}
// moved the `override virtual` before the `public` modifier
function transfer(address recipient, uint256 amount) override virtual public returns (bool) {
if(_transactionMaxValue > 0){
require(_transactionMaxValue >= amount, "You can not transfer more than 1000000 tokens at once!");
}
// added `return super.`
return super.transfer(recipient, amount);
}
}
Everything #Petr Hejda suggested is correct.
Additional improvement here would be to make a require statement fail reason string smaller and more precise, as it will result in the smaller bytecode, and therefore cheaper deployment.
I have created a basic ERC20 token by implementing OpenZeppelin as follow in ERC20.sol file:
pragma solidity ^0.6.4;
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v3.4.0/contracts/token/ERC20/ERC20.sol";
contract Token is ERC20 {
constructor(string memory _name, string memory _symbol)
public
ERC20(_name, _symbol)
{
_mint(msg.sender, 10000000000000000000000000000);
}
}
Then implement another contract Contract.sol as follow:
import "./ERC20.sol";
pragma solidity ^0.6.4;
contract SimpleBank{
Token tokenContract;
constructor(Token _tokenContract) public {
tokenContract = _tokenContract;
}
function deposit(uint amt) public returns (bool) {
require(amt != 0 , "deposit amount cannot be zero");
tokenContract.transfer(address(this),amt);
return true;
}
}
As, I have deployed both contract from the address 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2 so, it holds 10000000000000000000000000000 tokens.
But when I call deposit function from same address I got the following error:
transact to SimpleBank.deposit errored: VM error: revert. revert The
transaction has been reverted to the initial state. Reason provided by
the contract: "ERC20: transfer amount exceeds balance". Debug the
transaction to get more information.
So, what is the proper way to interact with the deployed ERC20 token so that the deploy function works.
The user address 0xAb8483... sends a transaction executing SimpleBank's function deposit(), which makes 0xAb8483... the value of msg.sender in SimpleBank.
But then SimpleBank sends an internal transaction executing Token's function transfer(). Which makes SimpleBank address (not the 0xAb8483...) the value of msg.sender in Token.
So the snippet tokenContract.transfer(address(this),amt); within SimpleBank is trying to send SimpleBank's tokens. Not the user's (0xAb8483...) tokens.
This transfer of tokens (from point 2) reverts, because SimpleBank doesn't own any tokens. Which makes the top-level transaction (from point 1) revert as well.
If you want SimpleBank to be able to transfer 0xAb8483...'s tokens, 0xAb8483... needs to approve() the tokens first to be spent by SimpleBank. Directly from their address, so that they are msg.sender in the Token contract.
Only then SimpleBank can execute transferFrom(0xAb8483..., address(this), amt) (from, to, amount).
TLDR: Your contract can't spend tokens that it doesn't own, unless the owner has manually approved your contract to spend them.
If it could spend someone else's tokens without approval, it would be very easy to steal from people who can't/don't verify your source code (by spending their USDT, WETH and other widely-used tokens).
I came across this article dated 2019/9 about avoiding using solidity's transfer()/send(). Here is the reasoning from the article:
It looks like EIP 1884 is headed our way in the Istanbul hard fork. This change increases the gas cost of the SLOAD operation and therefore breaks some existing smart contracts.
Those contracts will break because their fallback functions used to consume less than 2300 gas, and they’ll now consume more. Why is 2300 gas significant? It’s the amount of gas a contract’s fallback function receives if it’s called via Solidity’s transfer() or send() methods. 1
Since its introduction, transfer() has typically been recommended by the security community because it helps guard against reentrancy attacks. This guidance made sense under the assumption that gas costs wouldn’t change, but that assumption turned out to be incorrect. We now recommend that transfer() and send() be avoided.
In remix, there is a warning message about the code below:
(bool success, ) = recipient.call{value:_amount, gas: _gas}("");
Warning:
Low level calls: Use of "call": should be avoided whenever possible. It can lead to unexpected behavior if return value is not handled properly. Please use Direct Calls via specifying the called contract's interface. more
I am not an expert on gas cost over execution of smart contract and security. So I post this article and would appreciate thoughts and comments about it.
First, it's good to know about the fallback function in Solidity:
It has no name, no arguments, no return value, and it can be defined as one per contract, but the most important feature is it is called when a non-existent function is called on the contract such as to send or transfer or call.value()(""). So if you want to send Ether directly to an address which is a contract address, the fallback function of the destination contract will be called.
If the contract's fallback function not marked payable, it will throw an exception if the contract receives plain ether without data.
Now let's look at the reentrancy attack
contract VulnerableContract {
mapping(address => uint) public balances;
function deposit() public payable {
require(msg.value > 1);
balances[msg.sender] += msg.value;
}
function withdraw(uint _amount) public {
require(balances[msg.sender] >= _amount, "Not enough balance!");
msg.sender.call.value(_amount)("");
balances[msg.sender] -= _amount;
}
function getBalance() view public returns(uint) {
return address(this).balance;
}
fallback() payable external {}
}
VuinerableContract has a withdraw function which sends Ether to the calling address. Now the calling address could be a malicious contract such as this :
contract MaliciousContract {
VulnerableContract vulnerableContract = VulnerableContract(0x08970FEd061E7747CD9a38d680A601510CB659FB);
function deposit() public payable {
vulnerableContract.deposit.value(msg.value)();
}
function withdraw() public {
vulnerableContract.withdraw(1 ether);
}
function getBalance() view public returns(uint) {
return address(this).balance;
}
fallback () payable external {
if(address(vulnerableContract).balance > 1 ether) {
vulnerableContract.withdraw(1 ether);
}
}
}
When the malicious contract call the withdraw function, before reducing the balance of the malicious contract, it's fallback function will be called at it can steal more Ether from the vulnerable contract.
So by limiting the gas amount used by the fallback function up to 2300 gas we could prevent this attack. It means we can not put complex and expensive commands in the fallback function anymore.
Look at this for more information: https://swcregistry.io/docs/SWC-107
From Consensys article, they are saying use .call() instead of .transfer() and .send(). Only argument is that all three now sends more gas than 2300. Thus making it possible for reentrancy.
This comes to another conclusion that, regardless of all above, it is important to use checks-effects-interactions pattern to prevent reentracy attack.
I have a simple ERC20 contract which is ERC20Detailed, ERC20Mintable and Ownable. On deployment I want:
Add Minter to a new account, passed in the argument
Remove deployer from Minter role
Transfer Ownership to a new account, passed in the argument
Same actions I have declared in another function called transferRights()
The problem is that I am getting "Gas estimation failed error", which isn't because I don't have enough gas, but there might be a bug in the code. If I remove first two (addMinter, renounceMinter) actions, then it's all good (No warning).
I have deployed this contract on Ropsten, where I was getting same error in the beginning, but by commenting first two actions and adding them again transaction went through without any warning and Contract works as it supposed to be.
pragma solidity ^0.5.0;
import "github.com/OpenZeppelin/openzeppelin-solidity/contracts/token/ERC20/ERC20Detailed.sol";
import "github.com/OpenZeppelin/openzeppelin-solidity/contracts/token/ERC20/ERC20Mintable.sol";
import "github.com/OpenZeppelin/openzeppelin-solidity/contracts/ownership/Ownable.sol";
contract MyToken is ERC20Detailed, ERC20Mintable, Ownable {
uint256 private msgCount;
address constant ETHER = address(0);
mapping(uint256 => string) private message;
constructor(string memory name, string memory symbol, uint8 decimals, address _newOwner) ERC20Detailed(name, symbol, decimals) public {
addMinter(_newOwner);
renounceMinter();
transferOwnership(_newOwner);
}
function doMint(uint256 _amount, address _beneficiary1, address _beneficiary2, address _beneficiary3) public onlyOwner {
require (_amount >= 0);
require(_beneficiary1 != ETHER && _beneficiary2 != ETHER && _beneficiary3 != ETHER);
require(mint(_beneficiary1, _amount.mul(20).div(100)));
require(mint(_beneficiary2, _amount.mul(30).div(100)));
require(mint(_beneficiary3, _amount.mul(50).div(100)));
}
function setMessage(string memory _message) public onlyOwner {
message[msgCount] = _message;
msgCount = msgCount.add(1);
}
function readMessage(uint256 _msgId) public view returns(string memory) {
return message[_msgId];
}
function transferRights(address _newOwner) public onlyOwner {
addMinter(_newOwner);
renounceMinter();
transferOwnership(_newOwner);
}
}
I can still send a transaction and deploy I guess (as mentioned above I did it on testnet), even though it is saying "The transaction execution will likely fail", but I want to make sure that code is bug free. Any feedback would be greatly appreciated. Thanks!
UPDATE
Problem found! In the constructor I was passing the same account address as I was using for deploying. Thus, it resulted adding the Minter role to myself, which I already had.