Internal and External
In addition to public
and private
, Solidity has two more types of visibility for functions: internal
and external
.
internal
is the same as private
, except that it's also accessible to contracts that inherit from this contract. (Hey, that sounds like what we want here!).
external
is similar to public
, except that these functions can ONLY be called outside the contract — they can't be called by other functions inside that contract. We'll talk about why you might want to use external
vs public
later.
For declaring internal
or external
functions, the syntax is the same as private
and public
:
contract Sandwich {
uint private sandwichesEaten = 0;
function eat() internal {
sandwichesEaten++;
}
}
contract BLT is Sandwich {
uint private baconSandwichesEaten = 0;
function eatWithBacon() public returns (string memory) {
baconSandwichesEaten++;
// We can call this here because it's internal
eat();
}
}
Interacting with other contracts
For our contract to talk to another contract on the blockchain that we don't own, first we need to define an interface.
Let's look at a simple example. Say there was a contract on the blockchain that looked like this:
contract LuckyNumber {
mapping(address => uint) numbers;
function setNum(uint _num) public {
numbers[msg.sender] = _num;
}
function getNum(address _myAddress) public view returns (uint) {
return numbers[_myAddress];
}
}
This would be a simple contract where anyone could store their lucky number, and it will be associated with their Ethereum address. Then anyone else could look up that person's lucky number using their address.
Now let's say we had an external contract that wanted to read the data in this contract using the getNum
function.
First we'd have to define an interface of the LuckyNumber
contract:
contract NumberInterface {
function getNum(address _myAddress) public view returns (uint);
}
Notice that this looks like defining a contract, with a few differences. For one, we're only declaring the functions we want to interact with — in this case getNum
— and we don't mention any of the other functions or state variables.
Secondly, we're not defining the function bodies. Instead of curly braces ({
and }
), we're simply ending the function declaration with a semi-colon (;
).
So it kind of looks like a contract skeleton. This is how the compiler knows it's an interface.
By including this interface in our dapp's code our contract knows what the other contract's functions look like, how to call them, and what sort of response to expect.
Using an Interface
contract NumberInterface {
function getNum(address _myAddress) public view returns (uint);
}
We can use it in a contract as follows:
contract MyContract {
address NumberInterfaceAddress = 0xab38...
// ^ The address of the FavoriteNumber contract on Ethereum
NumberInterface numberContract = NumberInterface(NumberInterfaceAddress);
// Now `numberContract` is pointing to the other contract
function someFunction() public {
// Now we can call `getNum` from that contract:
uint num = numberContract.getNum(msg.sender);
// ...and do something with `num` here
}
}
In this way, your contract can interact with any other contract on the Ethereum blockchain, as long they expose those functions as public
or external
.
Handling Multiple Return Values
function multipleReturns() internal returns(uint a, uint b, uint c) {
return (1, 2, 3);
}
function processMultipleReturns() external {
uint a;
uint b;
uint c;
// This is how you do multiple assignment:
(a, b, c) = multipleReturns();
}
// Or if we only cared about one of the values:
function getLastReturnValue() external {
uint c;
// We can just leave the other fields blank:
(,,c) = multipleReturns();
}
If statements
If statements in Solidity look just like JavaScript:
function eatBLT(string memory sandwich) public {
// Remember with strings, we have to compare their keccak256 hashes
// to check equality
if (keccak256(abi.encodePacked(sandwich)) == keccak256(abi.encodePacked("BLT"))) {
eat();
}
}
Immutability of Contracts
Solidity has looked quite similar to other languages like JavaScript. But there are a number of ways that Ethereum DApps are actually quite different from normal applications.
To start with, after you deploy a contract to Ethereum, it’s immutable, which means that it can never be modified or updated again.
The initial code you deploy to a contract is there to stay, permanently, on the blockchain. This is one reason security is such a huge concern in Solidity. If there's a flaw in your contract code, there's no way for you to patch it later. You would have to tell your users to start using a different smart contract address that has the fix.
But this is also a feature of smart contracts. The code is law. If you read the code of a smart contract and verify it, you can be sure that every time you call a function it's going to do exactly what the code says it will do. No one can later change that function and give you unexpected results.
External dependencies
we hard-coded the CryptoKitties contract address into our DApp. But what would happen if the CryptoKitties contract had a bug and someone destroyed all the kitties?
It's unlikely, but if this did happen it would render our DApp completely useless — our DApp would point to a hardcoded address that no longer returned any kitties. Our zombies would be unable to feed on kitties, and we'd be unable to modify our contract to fix it.
For this reason, it often makes sense to have functions that will allow you to update key portions of the DApp.
For example, instead of hard coding the CryptoKitties contract address into our DApp, we should probably have a setKittyContractAddress
function that lets us change this address in the future in case something happens to the CryptoKitties contract.
Ownable Contracts
setKittyContractAddress
is external
, so anyone can call it! That means anyone who called the function could change the address of the CryptoKitties contract, and break our app for all its users.
We do want the ability to update this address in our contract, but we don't want everyone to be able to update it.
To handle cases like this, one common practice that has emerged is to make contracts Ownable
— meaning they have an owner (you) who has special privileges.
OpenZeppelin's Ownable
contract
Below is the Ownable
contract taken from the OpenZeppelin Solidity library. OpenZeppelin is a library of secure and community-vetted smart contracts that you can use in your own DApps. After this lesson, we highly recommend you check out their site to further your learning!
Give the contract below a read-through. You're going to see a few things we haven't learned yet, but don't worry, we'll talk about them afterward.
/**
* @title Ownable
* @dev The Ownable contract has an owner address, and provides basic authorization control
* functions, this simplifies the implementation of "user permissions".
*/
contract Ownable {
address private _owner;
event OwnershipTransferred(
address indexed previousOwner,
address indexed newOwner
);
/**
* @dev The Ownable constructor sets the original `owner` of the contract to the sender
* account.
*/
constructor() internal {
_owner = msg.sender;
emit OwnershipTransferred(address(0), _owner);
}
/**
* @return the address of the owner.
*/
function owner() public view returns(address) {
return _owner;
}
/**
* @dev Throws if called by any account other than the owner.
*/
modifier onlyOwner() {
require(isOwner());
_;
}
/**
* @return true if `msg.sender` is the owner of the contract.
*/
function isOwner() public view returns(bool) {
return msg.sender == _owner;
}
/**
* @dev Allows the current owner to relinquish control of the contract.
* @notice Renouncing to ownership will leave the contract without an owner.
* It will not be possible to call the functions with the `onlyOwner`
* modifier anymore.
*/
function renounceOwnership() public onlyOwner {
emit OwnershipTransferred(_owner, address(0));
_owner = address(0);
}
/**
* @dev Allows the current owner to transfer control of the contract to a newOwner.
* @param newOwner The address to transfer ownership to.
*/
function transferOwnership(address newOwner) public onlyOwner {
_transferOwnership(newOwner);
}
/**
* @dev Transfers control of the contract to a newOwner.
* @param newOwner The address to transfer ownership to.
*/
function _transferOwnership(address newOwner) internal {
require(newOwner != address(0));
emit OwnershipTransferred(_owner, newOwner);
_owner = newOwner;
}
}
A few new things here we haven't seen before:
- Constructors:
constructor()
is a constructor, which is an optional special function. It will get executed only one time, when the contract is first created. - Function Modifiers:
modifier onlyOwner()
. Modifiers are kind of half-functions that are used to modify other functions, usually to check some requirements prior to execution. In this case, onlyOwner
can be used to limit access so only the owner of the contract can run this function. We'll talk more about function modifiers in the next chapter, and what that weird _;
does. indexed
keyword: don't worry about this one, we don't need it yet.
So the Ownable
contract basically does the following:
When a contract is created, its constructor sets the owner
to msg.sender
(the person who deployed it)
It adds an onlyOwner
modifier, which can restrict access to certain functions to only the owner
It allows you to transfer the contract to a new owner
onlyOwner
is such a common requirement for contracts that most Solidity DApps start with a copy/paste of this Ownable
contract, and then their first contract inherits from it.
Since we want to limit setKittyContractAddress
to onlyOwner
, we're going to do the same for our contract.
No comments:
Post a Comment