Skip to content

Latest commit

 

History

History
237 lines (169 loc) · 11.6 KB

File metadata and controls

237 lines (169 loc) · 11.6 KB

Lesson 06

Solidity Events

Your smart contract is ready to be used, since you have confidence in the code after testing it.

But there are a few things that you can add to improve the behavior and security of it.

Let's start by adding events.

What is an Event in solidity?

Similar to any other programming language Events in solidity are a way to inform the calling application about the current state of the contract. Like notifying the javascript client about a change made to the contract allowing the client application to react to it.

Events are defined within the contract as a global and are called from inside a function. To declare an event you use the event keyword, followed by an identifier and the parameter list. The values passed to the parameters are then used to log the information that is saved as part of the transaction inside the block.

To send the event you'll use the emit keyword from anywhere inside the contract by passing the name of the event and the corresponding parameters.

For this contract you want to let the client application to know when a tip was successfully sent. To do that you'll create a new event named NewTip like this

event NewTip(address indexed from, string message, string name, uint256 amount);

It will accept an address to identify who sent the tip, the message of the tip and also the name and the amount of eth sent,

And then, it will be emitted from the sendTip function

// send the event
emit NewTip(msg.sender, _message, _name, msg.value);

This event will use the same parameters that the rest of the sendTip operation.

  • msg.sender to get the address of who sent the tip
  • _message the string with the message sent
  • _name the name of the sender
  • msg.value the amount of eth sent.

Testing the event

Sice you modify your contract, you need to also update your tests, in this case is about checking that the event is actually send. To accomplish that, let's create a new test case under test/TipJar.js with the following content

it('should react to the tip event', async function () {
	const [, sender] = await ethers.getSigners(); // Get the address of the sendder
	const amount = ethers.utils.parseEther('0.1');
	const tipContract = contract.connect(sender);
	const tx = await tipContract.sendTip('event message', 'name', { value: amount }); // Perform the transaction
	await tx.wait();
	expect(tx).to.emit(contract, 'NewTip').withArgs(sender.address, 'event message', 'name', amount); // Check that the event was sent
});

The last line of the scenario is the important one here, it allow you to check if an event was triggered or not.

Improvements: Gas and Security

The contract is ready to be ship, but you can do a little bettter. An smart contract is identify by an address, same as you, the owner of the contract. This address is the identifier of an account and an account can store a balance, meaning that the smart contract can also store a balance of some eth. Why is this important? Because you can tweak the contract a bit to instead of using it as a bridge to allow someone to send some eth directly to you it will store the tip in the contract itself and then allow you to withdraw that amount whenever you want.

Why you'll want to do something like this? To lower the gas costs. By doing this the total gast cost of the operation will be lesser for the sender since the you (the owner of the contract) will share some of the gas for retrieving the funds from the contract. So, is a way to care about the people that want to support you through your tip jar.

Ok, you're convicend? Yes?. Cool, now you're wondering. How can I and only I withdraw the funds ?

To do that, you'll need to create a new function in the contract that send the funds from the contract to your address but that ensures that only you can do it.

This can be done by checking the address of the function caller msg.sender against the owner variable that holds the owner address. Something like

function withdraw() public{
	require(owner == msg.sender, 'caller is not the owner');
}

That require statement will ensure that only the owner can execute whatever code is under it. Good, you just added some security measure to the widthraw function.

This is a good time to introduce a new Solidity concept: Modifiers

Modifiers

A function modifier is a way to change the behavior of a function without actually updating the body of the function code (like the decorator pattern). The modifier is attached to the function definition to change the function's behavior.

A modifier looks just like a function buyt use the keyword modifier instead and it can't be directly called.

Why you want modifiers? Is a simple way to create reusable pieces of code that can be shared across your contract. In this case you have only one function that requires ownership but, if in the future you want to add a few more functions you can benefit by having the ownership requirement as a shareable code.

This is known as "Access control" - that is "who is allowed to do this thing" - is really important in the world of smart contracts, it may govern who can mint tokens, vote, withdraw, transfer, etc. OpenZeppellin contracts ofer utilities to handle access control. What you'll see in this course is the very basic form of access control: ownership.

Let's create a modifir based on the prior require statement

modifier onlyOwner() {
	require(owner === msg.sender, "caller is not the owner");
	_;
}

function withdraw() public onlyOwner {

}

This modifier is really simple, only one require statement and no arguments. To create a modifier you need to use the modifier keyword and end the block with the _; symbol. This symbol is called a merge wildcard. It merges the function code with the modifier code where the _; is placed (A modifier is a compile-time source code roll-up).

In other words, the body of the function will be inserted in the place where the symbol appears.

The symbol is mandatory for the modifier but can be placed anywhere inside the modifier body, depending on when the function body has to be exeecuted.

Lets store the eth and withdraw

Now, you need to modify the current sendTip function to instead of immediatly send the eth to your account, store it in the contract.

Let`s change the function like this

//Update the sendTip function
function sendTip(string memory _message, string memory _name) public payable {
		require(msg.sender.balance >= msg.value, "Youd don't have enough funds"); // require that the sender has enough ether to send
		// THIS IS THE CHANGE
		// The `call` function was removed

		// END OF THE CHANGE
		totalTips += 1; //increase the amount of tips
		tips.push(Tip(msg.sender, _message, _name, block.timestamp, msg.value)); // Store the tip

		// send the event
		emit NewTip(msg.sender, _message, _name, msg.value);
}

The main change is that the usage of call method was removed, so: How the eth is stored in the contract? This is a solitidy feature. Since sending eth is the basic use case of a contract, if a function is payable and msg.value have some value, then that value is stored in the contract immediately :D.

Withdraw the funds

Now, the contract will act as centralized bank that store all the money sent to you, but you don't actually have it in your account. You need a way to extract that money, that is the withdraw function that you drafted above.

What will be the body of that function?

// lets add a new event
event NewWithdrawl(uint256 amount);

/*
	 * Allow the owner to withdraw all the ether in the contract
	 */
	function withdraw() public onlyOwner {
		uint256 amount = address(this).balance; // get the amount of ether in the contract
		require(amount > 0, 'You have no ether to withdraw');
		(bool success, ) = owner.call{value: amount}('');
		require(success, 'Withdraw failed');
		emit NewWithdrawl(amount);
	}

Is actually simple. It first get the balance of the contract, then will check if there is something to withdraw and if so will perform the transfer call to your address for the whole amount of the contract balance. Then will check if the transaction was successful and will emit an event to notify of it.

Update the tests

Since the contract change again, you need to update the tests. The main change will be in the second tests, the one that check the balances. Now instead of checking the owner balance, should check the contract balance.

it('Should allow to send a tip and increase the number of total tips', async function () {
	const [owner, sender] = await ethers.getSigners(); // Get two addresses, the owner and the sender
	const balance = await ethers.provider.getBalance(contract.address); // Get the account balance of the contract
	const senderBalance = await sender.getBalance();
	/*
	 * perform the send transaction
	 * You pass the message and the name as arguments and the value as an object that is then
	 * used as the global `msg` object in the contract
	 * to define the amount of ETH use the parseEther utility
	 */
	const tx = await contract
		.connect(sender)
		.sendTip('message', 'name', { value: ethers.utils.parseEther('0.001') });
	await tx.wait();

	const newBalance = await ethers.provider.getBalance(contract.address); // Get the new balance of the contract
	const newSenderBalance = await sender.getBalance();
	expect(newBalance).to.be.above(balance); // Check that the new balance if greater than before
	expect(newSenderBalance).to.be.below(senderBalance); // Check that the sender balance is less than before
	expect(await contract.getTotalTips()).to.equal(1); // Get the total number of tips
});

Will also add two more tests, one to check if the owner can withdraw the balance and other to check that no one else than the owner can withdraw

it('should allow the owner to withdraw the whole balance', async function () {
	const [owner] = await ethers.getSigners();
	const ownerBalance = await owner.getBalance(); // get the owner balance
	const originalContractBalance = await ethers.provider.getBalance(contract.address); // Get the contract balance
	const tips = await contract.getAllTips(); //Get all the tips
	const sumTips = tips.reduce((acc, tip) => acc.add(tip.amount), ethers.BigNumber.from(0)); // Get the total amount of tips
	expect(sumTips).to.be.equals(originalContractBalance); //Since this is the first widthdraw the sum of the tips should be the same as the contract balance
	/*
	 * perform the withdraw transaction
	 */
	const tx = await contract.withdraw();
	await tx.wait();

	const newOwnerBalance = await owner.getBalance(); // Get the new balance of the owner
	const contractBalance = await ethers.provider.getBalance(contract.address); // Get the account balance of the contract

	expect(newOwnerBalance).to.be.above(ownerBalance); // Check that the new balance if greater than before
	expect(contractBalance).to.be.equal(0); // Check that the contract balance is zero

	// Since there is no balance in the contract
	// the withdraw should fail
	await expect(contract.withdraw()).to.be.reverted;
});

it('should reject withdrawal from another address different than owner', async function () {
	const [, otherUser] = await ethers.getSigners();
	// Try to withdraw again
	// it should fail since there is no eth in the contract
	await expect(contract.connect(otherUser).withdraw()).to.be.reverted;
});

Run the test in the terminal

$ npm run hardhat:test

And check the results