The idea of "gasless" transactions is that a user signs a message, and sends that message to a gas relayer. The relayer is a separate entity, which collects the messages from the users, and sends the transactions. This way, the user doesn't need to pay for the transaction, only the relayer does. Then, optionally, the user can pay in tokens or other means of payment to the relayer.
The users' funds are stored in a smart contract, and only with a valid signature provided by the user, can funds be sent from that contract. In your example, you're calling a function on a contract as the user. Since all actual transactions on the network require gas, this will simply send a transaction directly to the contract, rather than using a gas relayer.
To send transactions to the gas station network, you can use their JavaScript libraries, for example:
const { RelayProvider } = require('@opengsn/gsn')
const provider = new RelayProvider(web3.currentProvider, ...);
const web3 = new Web3(provider);
const counterContract = new web3.eth.Contract(abi, ...);
await counterContract.methods.addValue().send({ ... });
This will use the RelayProvider
to sign the transaction and send it to a gas relayer, who will then send the transaction to your contract, on behalf of the user.
There's a more detailed guide for using OpenGSN here: https://docs.opengsn.org/gsn-provider/getting-started.html
First of all, let's create a basic re-entrancy vulnerable contract:
contract Safe {
mapping(address => uint) public userBalances;
function deposit() public payable {
userBalances[msg.sender] += msg.value;
}
function withdraw(uint _amount) public {
require(userBalances[msg.sender] >= _amount, "low balance");
payable(msg.sender).call({value: _amount});
userBalances[msg.sender] -= _amount;
}
}
As you can see we didn't set the max gas that call
can use. What does it mean? You can restrict the gas amount that external function calls can use. Take a look at here. What if you did not set this? It can use all over of your gas (nearly 1m gas). Is it bad? It can be. Fallback functions are for what should contract do when it receives ether
(not just that but it is enough for now to now that).
We are a popular DeFi project. So, there will be thousands of ether in our contract.
This attacker creates a contract for attack to our Safe contract. And this contract has this function:
function attack() public onlyOwner {
Safe.deposit({value: 1 ether});
Safe.withdraw(1 ether);
}
This function is basically to deposit and withdraw 1 ether for the attacker's smart contract. And our attacker's smart contract also has a fallback function:
fallback() external payable {
Safe.withdraw(1 ether);
}
Okay, let's take a deep breath and look step by step what are we doing:
- Create an attacker contract
- Call attack function - Which means deposit and withdraw 1 ether.
- When we try to withdraw, the Safe contract tries to call our fallback function. And that function also tries to call withdraw function.
Can you see that? There is a loop. This loop will continue until all Safe contract funds are over. Because there will be an error. After then we will receive all the funds. Because our funds didn't decrease (because of our loop).
But if you'll change the call function line to this:
payable(msg.sender).call({value: _amount, gas: 30000});
Probably it will be okay (depending the gas calculation). But the best solution is first decrease userBalances, and after then send his money.
Best Answer
The code:
Is equivalent to:
If
func
is re-entered (i.e., called from within the execution ofstuff
), thenprolog
is executed a second time beforeepilog
was executed for the first time.So by identifying and reverting upon a scenario in which
prolog
is executed a second time beforeepilog
was executed for the first time, we can guardfunc
from being re-entered.