🏭 zombiefactory.sol
- 🍞 zombiefeeding.sol
- 👼 zombieHelper.sol
- 💥 zombieAttack.sol
- 🪙 erc721.sol
- 🧑🔬 safemath.sol
- 🧸 zombieOwnership.sol
🪙 erc721.sol
🧑🔬 safemath.sol
🚧 ownable.sol
ERC-20 token
과ERC-721 token
의 차이점
크립토 좀비 게임을 만들기에 적합한 token은 ERC-721이다
각각의 좀비가고유한 특성
을 들고 있고 하나의tokenId단위
로 거래되기 때문이다
솔리디티에서 overriding은 상속받은 contract의 메소드가 하위 contract에서 충분한 기능을 제공하지 않거나, 부족할 때 상위 contract로부터 상속받은 contract를 재정의 하는 것이다.
아래는 크립토 좀비 code는 아니고 override의 이해를 돕기위해 reference를 참고했다
// SPDX-License-Identifier:GPL-30
pragma solidity >= 0.7.0 < 0.9.0;
contract Father{
string public familyName = "Kim";
string public givenName = "Jung";
uint256 public money = 100;
constructor(string memory _givenName) public {
givenName = _givenName;
}
function getFamilyName() view public returns(string memory){
return familyName;
}
function getGivenName() view public returns(string memory){
return givenName;
}
//부모 컨트랙트에 virtual을 적어줘서 해당 함수가 overriding가능 함을 알려준다
function getMoney() view public virtual returns(uint256){
return money;
}
}
contract Son is Father("James"){
uint256 public earning = 0;
function work() public {
earning += 100;
}
//자식 컨트랙트에 override를 적어줘서 이 함수를 overriding할 것이라고 알려줌
function getMoney() view public override returns(uint256){
return money+earning;
}
}
부모 컨트랙트의 함수에
virtual
(이 함수는 자식 컨트랙트에서 overriding 가능하다)
자식 컨트랙트의 함수에override
(이 함수를 overriding할거다)
ERC-71
의 balanceOf
, ownerOf
함수balanceOf
: address를 받아, 해당 address가 토큰을 얼마나 가지고 있는지 반환한다ownerOf
: 토큰 ID(우리의 경우에는 좀비 ID)를 받아, 이를 소유하고 있는 사람의 address를 반환한다.
ERC-721 표준
에서 정의하는 함수 이름과
내가 개인적으로 작성한smart contract
의 함수이름이중복
되면 무엇을 바꿔야 할까?예를 들어, ERC-721 표준에서 정의하는 함수 중 ownerOf와 내가 크립토 좀비를 만들며 작성한 modifier의 이름 ownerOf가 이름이 중복된다고 해보자
이런 상황에서는
내가 개인적으로 작성한 함수의 이름을 바꾸는 것이 바람직하다
왜냐하면ERC-721
은 다른 token 네트워크들과도 원활하게 상호작용할 수 있어야하기 때문이다
만약 내가 배포하는ERC-721 token
의 함수이름을 고친다면 다른 네트워크와 기능적으로 충돌이 생길 것이다.
pragma solidity ^0.4.19;
import "./zombieattack.sol";
import "./erc721.sol";
contract ZombieOwnership is ZombieAttack, ERC721 {
function balanceOf(address _owner) public view returns (uint256 _balance) {
return ownerZombieCount[_owner];
}
function ownerOf(uint256 _tokenId) public view returns (address _owner) {
return zombieToOwner[_tokenId];
}
//크립토 좀비 게임에 맞는 transfer함수를 만들기 위해 _transfer에서 부가적인 기능을 추가했다(추상화)
function _transfer(address _from, address _to, uint256 _tokenId) private {
ownerZombieCount[_to]++;
ownerZombieCount[_from]--;
zombieToOwner[_tokenId] = _to;
Transfer(_from, _to, _tokenId);
}
//_tokenId 보유자만 transfer함수를 통해 해당 zombie를 전송할 수 있도록 modifier onlyOwnerOf를 적용했다
function transfer(address _to, uint256 _tokenId) public onlyOwnerOf(_tokenId) {
_transfer(msg.sender, _to, _tokenId);
}
function approve(address _to, uint256 _tokenId) public {
}
function takeOwnership(uint256 _tokenId) public {
}
}
function transfer(address _to, uint256 _tokenId) public;
첫 번째 방법은 토큰의 소유자가 전송 상대의
address
, 전송하고자 하는_tokenId
와 함께transfer
함수를 호출하는 것이다.
function approve(address _to, uint256 _tokenId) public;
function takeOwnership(uint256 _tokenId) public;
두 번째 방법은 토큰의 소유자가 먼저 위에서 본 정보들을 가지고
approve
를 호출하는 것다. 그리고서 컨트랙트에 누가 해당 토큰을 가질 수 있도록 허가를 받았는지 저장한다. 보통mapping (uint256 => address)
를 써서 저장한다. 이후 누군가takeOwnership
을 호출하면, 해당 컨트랙트는 이msg.sender
가 소유자로부터 토큰을 받을 수 있게 허가를 받았는지 확인한다. 그리고 허가를 받았다면 해당 토큰을 그에게 전송한다.
event
라는 것은 블록체인 네트워크의 블록에 특정값을 기록하는 것을 말한다.
log 기록
을 남기기 위해 event를 사용한다고 생각하면 될 것 같다(consolelog).
예를 들어,Transaction이라는 event
가 있다고 가정하였을때,Transaction()
을 실행시키면, 해당 송금 transaction에 대한 from(송신자), to(수신자), value(금액 or NFT tokenId)의 정보가 event로 출력이 되어서(emit
으로 출력, emit안적으면 log안남음) 블록체인 네트워크 안에 기록이 된다.
pragma solidity ^0.4.19;
import "./zombieattack.sol";
import "./erc721.sol";
contract ZombieOwnership is ZombieAttack, ERC721 {
// zombieApprovals이라는 이름으로 approve된 좀비들을 저장할 mapping을 만들었다
mapping (uint => address) zombieApprovals;
function balanceOf(address _owner) public view returns (uint256 _balance) {
return ownerZombieCount[_owner];
}
function ownerOf(uint256 _tokenId) public view returns (address _owner) {
return zombieToOwner[_tokenId];
}
function _transfer(address _from, address _to, uint256 _tokenId) private {
ownerZombieCount[_to]++;
ownerZombieCount[_from]--;
zombieToOwner[_tokenId] = _to;
Transfer(_from, _to, _tokenId);
}
function transfer(address _to, uint256 _tokenId) public onlyOwnerOf(_tokenId) {
_transfer(msg.sender, _to, _tokenId);
}
// _tokenId보유자만 approve함수를 실행시킬 수 있어야 함으로 modifier onlyOwnerOf를 적용했다
function approve(address _to, uint256 _tokenId) public onlyOwnerOf(_tokenId) {
// zombieApprovals mappring에 내가 보내고자하는 _tokenId를 수신자 _to에게 전달할 것을 저장한다
zombieApprovals[_tokenId] = _to;
Approval(msg.sender, _to, _tokenId);
}
function takeOwnership(uint256 _tokenId) public {
// 입력받은 좀비(_tokenId)가 zombieApprovals mapping에 msg.sender주소로 들어가 있는지 확인한다(takeOwnership 함수를 실행하는 사람이 _tokenId의 소유권이 있는지 확인한다)
require(zombieApprovals[_tokenId] = msg.sender);
// 보내고자하는 좀비(_tokenId)의 소유권 주소가 어디인지 찾아서 owner 변수에 저장한다
address owner = ownerOf(_tokenId);
// 좀비(_tokenId)의 owner로부터 takeOwnership함수를 실행하는 실행자의 주소로 좀비(_tokenId)를 보낸다
_transfer(owner, msg.sender, _tokenId);
}
}
approve / takeOwnership
을 사용하는 전송은 2단계로 나뉜다는 것을 기억해야한다
소유자는 새로운 소유자의address
와 그에게 보내고 싶은_tokenId
를 사용하여approve
를 호출한다.
새로운 소유자가_tokenId
를 사용하여takeOwnership
함수를 호출하면, 컨트랙트는 그가 승인된 자인지 확인하고 그에게 토큰을 전송한다.
이처럼 2번의 함수 호출이 발생하기 때문에, 우리는 함수 호출 사이에 누가 무엇에 대해 승인이 되었는지 저장할 데이터 구조가 필요할 것이다(mapping으로 구현함).
마지막 함수인 takeOwnership에서는 msg.sender가 이 토큰(좀비)를 가질 수 있도록 승인되었는지 확인하고(zombieApprovals mapping 데이터 구조에 들어가 있는지 확인한다), 승인이 되었다면(zombieApprovals[_tokenId] = _to라면) _transfer를 호출해야 한다.
pragma solidity ^0.4.19;
import "./ownable.sol";
// safeMath 라이브러리가 적힌 sol파일을 import
import "./safemath.sol";
contract ZombieFactory is Ownable {
// SafeMath 라이브러리를 사용하기 위해 uint256에 대해 SafeMath method를 사용하겠다고 명시한다
using SafeMath for uint256;
...
}
그리고 using SafeMath for uint256;
을 적어줌으로써 uint256에서 다음과 같이 이 함수들에 접근할 수 있다.
using SafeMath for uint256;
uint256 a = 5;
uint256 b = a.add(3); // 5 + 3 = 8
uint256 c = a.mul(2); // 5 * 2 = 10
pragma solidity ^0.4.19;
import "./zombieattack.sol";
import "./erc721.sol";
contract ZombieOwnership is ZombieAttack, ERC721 {
...
function _transfer(address _from, address _to, uint256 _tokenId) private {
// ownerZombieCount[_to]++;에서 아래와 같이 교체
ownerZombieCount[_to] = ownerZombieCount[_to].add(1);
// ownerZombieCount[_from]--;에서 아래와 같이 교체
ownerZombieCount[_from] = ownerZombieCount[_from].sub(1);
zombieToOwner[_tokenId] = _to;
Transfer(_from, _to, _tokenId);
}
...
}
SafeMath 라이브러리
를 살펴보면 add, sub, mul, div 함수에assert로 에러처리
해준 것을 확인할 수 있다.assert는 조건을 만족하지 않으면 에러를 발생시킨다는 점에서 require와 비슷하다.
assert와 require의 차이점
은,
require는 함수 실행이 실패하면 남은 가스를 사용자에게 되돌려 주지만,
assert는 그렇지 않다는 것이다.
즉 대부분 경우 코드에 require를 쓰는 것이 이로울 것이다.
때문에assert는 일반적으로 코드가 심각하게 잘못 실행될 때 사용한다
(uint 오버플로우처럼 말이다).
간단히 말해
, SafeMath의 add, sub, mul, 그리고 div는 4개의 기본 수학 연산을 하는 함수이지만, 오버플로우나 언더플로우가 발생하면 에러를 발생시켜서 이를 방지한다.오버플로우나 언더플로우를 막기 위해 코드에서 +, -, * 또는 /을 쓰는 곳을 찾아 add, sub, mul, div로 교체하면 overflow, underflow없이 연산이 가능하다(정확하게는 오버플로우 언더플로우가 일어나면 에러처리를 해준다).
pragma solidity ^0.4.18;
/**
* @title SafeMath
* @dev Math operations with safety checks that throw on error
*/
library SafeMath {
/**
* @dev Multiplies two numbers, throws on overflow.
*/
function mul(uint256 a, uint256 b) internal pure returns (uint256) {
if (a == 0) {
return 0;
}
uint256 c = a * b;
assert(c / a == b);
return c;
}
/**
* @dev Integer division of two numbers, truncating the quotient.
*/
function div(uint256 a, uint256 b) internal pure returns (uint256) {
// assert(b > 0); // Solidity automatically throws when dividing by 0
uint256 c = a / b;
// assert(a == b * c + a % b); // There is no case in which this doesn't hold
return c;
}
/**
* @dev Subtracts two numbers, throws on overflow (i.e. if subtrahend is greater than minuend).
*/
function sub(uint256 a, uint256 b) internal pure returns (uint256) {
assert(b <= a);
return a - b;
}
/**
* @dev Adds two numbers, throws on overflow.
*/
function add(uint256 a, uint256 b) internal pure returns (uint256) {
uint256 c = a + b;
assert(c >= a);
return c;
}
}
/**
* @title SafeMath32
* @dev SafeMath library implemented for uint32
*/
library SafeMath32 {
function mul(uint32 a, uint32 b) internal pure returns (uint32) {
if (a == 0) {
return 0;
}
uint32 c = a * b;
assert(c / a == b);
return c;
}
function div(uint32 a, uint32 b) internal pure returns (uint32) {
// assert(b > 0); // Solidity automatically throws when dividing by 0
uint32 c = a / b;
// assert(a == b * c + a % b); // There is no case in which this doesn't hold
return c;
}
function sub(uint32 a, uint32 b) internal pure returns (uint32) {
assert(b <= a);
return a - b;
}
function add(uint32 a, uint32 b) internal pure returns (uint32) {
uint32 c = a + b;
assert(c >= a);
return c;
}
}
/**
* @title SafeMath16
* @dev SafeMath library implemented for uint16
*/
library SafeMath16 {
function mul(uint16 a, uint16 b) internal pure returns (uint16) {
if (a == 0) {
return 0;
}
uint16 c = a * b;
assert(c / a == b);
return c;
}
function div(uint16 a, uint16 b) internal pure returns (uint16) {
// assert(b > 0); // Solidity automatically throws when dividing by 0
uint16 c = a / b;
// assert(a == b * c + a % b); // There is no case in which this doesn't hold
return c;
}
function sub(uint16 a, uint16 b) internal pure returns (uint16) {
assert(b <= a);
return a - b;
}
function add(uint16 a, uint16 b) internal pure returns (uint16) {
uint16 c = a + b;
assert(c >= a);
return c;
}
}
Chapter 10
에서SafeMath
를 이용해서uint256 type
의 숫자들에 대해 overflow, underflow를 막는 방법을 익혔지만
uint8
uint16
uint 32
등의 숫자 type에 대해서overflow, underflow
를 막아야할 필요성을 느낄 때가 분명 있을 것이다.아래는 SafeMath의 기본 라이브러리 중 add함수를 적어놓은 것이다
function add(uint256 a, uint256 b) internal pure returns (uint256) { uint256 c = a + b; assert(c >= a); return c; }
만약
uint8
에.add
를 호출한다면, 타입이uint256
로 변환된다.
그러니 2^8에서 오버플로우가 발생하지 않을 것이다. 256은uint256
에서 유효한 숫자이기 때문이다.
결국 우리는 다른 type의 숫자들에 대해 오버플로우, 언더플로우를 막기 위해 uint16과 uint32에서 2개의 라이브러리를 더 만들어야 한다(위에 적혀있다).
(SafeMath16과 SafeMath32라고 불렀음)
pragma solidity ^0.4.19;
import "./zombieattack.sol";
import "./erc721.sol";
import "./safemath.sol";
/// @title 좀비 소유권 전송을 관리하는 컨트랙트
/// @author notoriousHong
contract ZombieOwnership is ZombieAttack, ERC721 {
/// @dev OpenZeppelin의 ERC721 표준 초안 구현을 따른다
...
}
솔리디티 커뮤니티에서 표준으로 쓰이는 주석형식은 natspec
이라 불린다.
아래와 같이 생겼다.
/// @title 작성한 컨트랙트의 제목을 적는다.
/// @author notoriousHong
/// @notice 사용자에게 컨트랙트/함수가 무엇을 하는지 설명한다
contract Math {
/// @notice 사용자에게 컨트랙트/함수가 무엇을 하는지 설명한다
/// @param x 첫 번쨰 uint. 함수에서 어떤 매개 변수와 반환값을 가지는지 설명
/// @param y 두 번째 uint. 함수에서 어떤 매개 변수와 반환값을 가지는지 설명
/// @return z (x * y) 곱의 값. 함수에서 어떤 매개 변수와 반환값을 가지는지 설명
/// @dev 개발자에게 추가적인 상세 정보를 설명하기 위해 사용한다.
function multiply(uint x, uint y) returns (uint z) {
// 이것은 일반적인 주석으로, natspec에 포함되지 않는다.
z = x * y;
}
}
pragma solidity ^0.4.19;
import "./zombiefeeding.sol";
contract ZombieHelper is ZombieFeeding {
uint levelUpFee = 0.001 ether;
modifier aboveLevel(uint _level, uint _zombieId) {
require(zombies[_zombieId].level >= _level);
_;
}
function withdraw() external onlyOwner {
owner.transfer(this.balance);
}
function setLevelUpFee(uint _fee) external onlyOwner {
levelUpFee = _fee;
}
function levelUp(uint _zombieId) external payable {
require(msg.value == levelUpFee);
zombies[_zombieId].level++;
}
function changeName(uint _zombieId, string _newName) external aboveLevel(2, _zombieId) onlyOwnerOf(_zombieId) {
zombies[_zombieId].name = _newName;
}
function changeDna(uint _zombieId, uint _newDna) external aboveLevel(20, _zombieId) onlyOwnerOf(_zombieId) {
zombies[_zombieId].dna = _newDna;
}
function getZombiesByOwner(address _owner) external view returns(uint[]) {
uint[] memory result = new uint[](ownerZombieCount[_owner]);
uint counter = 0;
for (uint i = 0; i < zombies.length; i++) {
if (zombieToOwner[i] == _owner) {
result[counter] = i;
counter++;
}
}
return result;
}
}
pragma solidity ^0.4.19;
import "./zombiefactory.sol";
contract KittyInterface {
function getKitty(uint256 _id) external view returns (
bool isGestating,
bool isReady<,
uint256 cooldownIndex,
uint256 nextActionAt,
uint256 siringWithId,
uint256 birthTime,
uint256 matronId,
uint256 sireId,
uint256 generation,
uint256 genes
);
}
contract ZombieFeeding is ZombieFactory {
KittyInterface kittyContract;
modifier onlyOwnerOf(uint _zombieId) {
require(msg.sender == zombieToOwner[_zombieId]);
_;
}
function setKittyContractAddress(address _address) external onlyOwner {
kittyContract = KittyInterface(_address);
}
function _triggerCooldown(Zombie storage _zombie) internal {
_zombie.readyTime = uint32(now + cooldownTime);
}
function _isReady(Zombie storage _zombie) internal view returns (bool) {
return (_zombie.readyTime <= now);
}
function feedAndMultiply(uint _zombieId, uint _targetDna, string _species) internal onlyOwnerOf(_zombieId) {
Zombie storage myZombie = zombies[_zombieId];
require(_isReady(myZombie));
_targetDna = _targetDna % dnaModulus;
uint newDna = (myZombie.dna + _targetDna) / 2;
if (keccak256(_species) == keccak256("kitty")) {
newDna = newDna - newDna % 100 + 99;
}
_createZombie("NoName", newDna);
_triggerCooldown(myZombie);
}
function feedOnKitty(uint _zombieId, uint _kittyId) public {
uint kittyDna;
(,,,,,,,,,kittyDna) = kittyContract.getKitty(_kittyId);
feedAndMultiply(_zombieId, kittyDna, "kitty");
}
}
pragma solidity ^0.4.19;
import "./ownable.sol";
import "./safemath.sol";
contract ZombieFactory is Ownable {
using SafeMath for uint256;
event NewZombie(uint zombieId, string name, uint dna);
uint dnaDigits = 16;
uint dnaModulus = 10 ** dnaDigits;
uint cooldownTime = 1 days;
struct Zombie {
string name;
uint dna;
uint32 level;
uint32 readyTime;
uint16 winCount;
uint16 lossCount;
}
Zombie[] public zombies;
mapping (uint => address) public zombieToOwner;
mapping (address => uint) ownerZombieCount;
function _createZombie(string _name, uint _dna) internal {
uint id = zombies.push(Zombie(_name, _dna, 1, uint32(now + cooldownTime), 0, 0)) - 1;
zombieToOwner[id] = msg.sender;
ownerZombieCount[msg.sender]++;
NewZombie(id, _name, _dna);
}
function _generateRandomDna(string _str) private view returns (uint) {
uint rand = uint(keccak256(_str));
return rand % dnaModulus;
}
function createRandomZombie(string _name) public {
require(ownerZombieCount[msg.sender] == 0);
uint randDna = _generateRandomDna(_name);
randDna = randDna - randDna % 100;
_createZombie(_name, randDna);
}
}
/**
* @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 public owner;
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
/**
* @dev The Ownable constructor sets the original `owner` of the contract to the sender
* account.
*/
function Ownable() public {
owner = msg.sender;
}
/**
* @dev Throws if called by any account other than the owner.
*/
modifier onlyOwner() {
require(msg.sender == owner);
_;
}
/**
* @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 {
require(newOwner != address(0));
OwnershipTransferred(owner, newOwner);
owner = newOwner;
}
}
pragma solidity ^0.4.19;
import "./zombiehelper.sol";
contract ZombieAttack is ZombieHelper {
uint randNonce = 0;
uint attackVictoryProbability = 70;
function randMod(uint _modulus) internal returns(uint) {
randNonce++;
return uint(keccak256(now, msg.sender, randNonce)) % _modulus;
}
function attack(uint _zombieId, uint _targetId) external onlyOwnerOf(_zombieId) {
Zombie storage myZombie = zombies[_zombieId];
Zombie storage enemyZombie = zombies[_targetId];
uint rand = randMod(100);
if (rand <= attackVictoryProbability) {
myZombie.winCount++;
myZombie.level++;
enemyZombie.lossCount++;
feedAndMultiply(_zombieId, enemyZombie.dna, "zombie");
} else {
myZombie.lossCount++;
enemyZombie.winCount++;
_triggerCooldown(myZombie);
}
}
}
contract ERC721 {
event Transfer(address indexed _from, address indexed _to, uint256 _tokenId);
event Approval(address indexed _owner, address indexed _approved, uint256 _tokenId);
function balanceOf(address _owner) public view returns (uint256 _balance);
function ownerOf(uint256 _tokenId) public view returns (address _owner);
function transfer(address _to, uint256 _tokenId) public;
function approve(address _to, uint256 _tokenId) public;
function takeOwnership(uint256 _tokenId) public;
}
pragma solidity ^0.4.18;
/**
* @title SafeMath
* @dev Math operations with safety checks that throw on error
*/
library SafeMath {
/**
* @dev Multiplies two numbers, throws on overflow.
*/
function mul(uint256 a, uint256 b) internal pure returns (uint256) {
if (a == 0) {
return 0;
}
uint256 c = a * b;
assert(c / a == b);
return c;
}
/**
* @dev Integer division of two numbers, truncating the quotient.
*/
function div(uint256 a, uint256 b) internal pure returns (uint256) {
// assert(b > 0); // Solidity automatically throws when dividing by 0
uint256 c = a / b;
// assert(a == b * c + a % b); // There is no case in which this doesn't hold
return c;
}
/**
* @dev Subtracts two numbers, throws on overflow (i.e. if subtrahend is greater than minuend).
*/
function sub(uint256 a, uint256 b) internal pure returns (uint256) {
assert(b <= a);
return a - b;
}
/**
* @dev Adds two numbers, throws on overflow.
*/
function add(uint256 a, uint256 b) internal pure returns (uint256) {
uint256 c = a + b;
assert(c >= a);
return c;
}
}
pragma solidity ^0.4.19;
import "./zombieattack.sol";
import "./erc721.sol";
import "./safemath.sol";
contract ZombieOwnership is ZombieAttack, ERC721 {
using SafeMath for uint256;
mapping (uint => address) zombieApprovals;
function balanceOf(address _owner) public view returns (uint256 _balance) {
return ownerZombieCount[_owner];
}
function ownerOf(uint256 _tokenId) public view returns (address _owner) {
return zombieToOwner[_tokenId];
}
function _transfer(address _from, address _to, uint256 _tokenId) private {
ownerZombieCount[_to] = ownerZombieCount[_to].add(1);
ownerZombieCount[msg.sender] = ownerZombieCount[msg.sender].sub(1);
zombieToOwner[_tokenId] = _to;
Transfer(_from, _to, _tokenId);
}
function transfer(address _to, uint256 _tokenId) public onlyOwnerOf(_tokenId) {
_transfer(msg.sender, _to, _tokenId);
}
function approve(address _to, uint256 _tokenId) public onlyOwnerOf(_tokenId) {
zombieApprovals[_tokenId] = _to;
Approval(msg.sender, _to, _tokenId);
}
function takeOwnership(uint256 _tokenId) public {
require(zombieApprovals[_tokenId] == msg.sender);
address owner = ownerOf(_tokenId);
_transfer(owner, msg.sender, _tokenId);
}
}