이더리움 스마트 컨트랙트를 활용해서, 간단한 기능을 가지는 지갑을 만들어보는 과제가 주어졌다. 사실 강의에서 과제에 대한 코드가 제시되지만, 이 코드를 보기 전에 스스로 만들어보고 강의를 진행하는 것을 추천받았다. 잘 만들 수 있을까 걱정이 앞서지만 일단 도전해보자. 요구사항은 다음과 같다.
- 입금은 누구나 가능하다.
- Owner는 자금을 무제한 인출 가능하다.
- Non-owner는 특정 주소의 특정 금액만 인출 가능하다.
- Owner만 Non-owner의 접근 권한을 바꿀 수 있다.
pragma solidity 0.8.0;
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/math/SafeMath.sol";
import "./Owned.sol";
/*
1. 입금은 누구나 가능하다.
2. Owner는 자금을 무제한 인출 가능하다.
3. Non-owner는 특정 주소의 특정 금액만 인출 가능하다.
4. Owner만 Non-owner의 접근 권한을 바꿀 수 있다.
*/
contract MyWallet is Owned {
using SafeMath for uint;
mapping(address => uint) public Balances;
event DepositEvent(address _addr, uint _amount);
event WithdrawEvent(address _addr, uint _amount);
function getSmartContractBalance() public view returns(uint) {
return address(this).balance;
}
function deposit(uint _amount) public payable {
require(address(msg.sender).balance >= _amount, "Not enough ether");
Balances[msg.sender] = Balances[msg.sender].add(_amount);
emit DepositEvent(msg.sender, _amount);
}
function withdrawalTo(address payable _to, uint _amount) public payable {
require(Balances[msg.sender] >= _amount, "Not enough ether");
Balances[msg.sender] = Balances[msg.sender].sub(_amount);
Balances[_to] = Balances[_to].add(_amount);
emit WithdrawEvent(msg.sender, _amount);
emit DepositEvent(_to, _amount);
_to.transfer(Balances[msg.sender]);
}
receive() external payable {
deposit(msg.value);
}
fallback () external {
}
}
pragma solidity ^0.8.0;
contract Owned {
address owner;
constructor() public {
owner = msg.sender;
}
/*
제어자.
밑줄 부분에 해당 제어자를 사용하는 함수 본문을 복사해온다.
그리고 제어자의 내용을 포함하여 다시 해당 함수로 복사한다.
너무 남용하면 코드가 복잡해지므로 주의한다.
*/
modifier onlyOwner() {
require(msg.sender == owner, "You are not allowed");
_;
}
}
스스로 지갑 스마트 컨트랙트를 만들어 보는게 쉽지 않았다. 왜냐면 2번, 4번 요구사항이 이해가 안 됐기 때문이다. 이유를 생각해보니 스마트 컨트랙트를 배포한 사람의 지갑 주소와 배포된 스마트 컨트랙트의 주소가 다르다는 것부터 잘못 이해하고 있었다. 그리고 지갑 스마트 컨트랙트를 만든다는 것은 이 지갑을 이용하는 사람마다 스마트 컨트랙트가 배포된다는 점도 이해하지 못 하고 있었다.
function deposit(uint _amount) public payable {
require(address(msg.sender).balance >= _amount, "Not enough ether");
Balances[msg.sender] = Balances[msg.sender].add(_amount);
emit DepositEvent(msg.sender, _amount);
}
...
receive() external payable {
deposit(msg.value);
}
그렇다보니 receive() 함수를 이용해서 스마트 컨트랙트에 이더리움을 입금 받는 단계에서 더이상 진행할 수가 없었다. receive()를 통해 받은 이더리움을 Balances[msg.sender]에 넣어서 관리하려고 했는데, 이렇게 되면 송신한 이더리움은 스마트 컨트랙트 자체에도 저장되고 Balances[msg.sender]에도 저장되었다. 즉 이중 지불 문제가 발생한 것이다. 그렇다고 Balances[msg.sender]에 저장하지 않으면, 입금된 이더리움을 활용할 수단이 없었다.
// solidity-ethereum-bootcamp with Thomas Wiesner
pragma solidity 0.8.0;
import './Allowance.sol';
contract SimpleWallet is Allowance {
event MoneySent(address indexed _beneficiary, uint indexed _amount);
event MoneyReceive(address indexed _from, uint indexed _amount);
function withdrawMoney(address payable _to, uint _amount) public ownerOrAllowed(_amount) {
require(address(this).balance >= _amount, "There are not enough funds stored in the smart contract");
if(!isOwner()) {
reduceAllowance(msg.sender, _amount);
}
MoneySent(_to, _amount);
_to.transfer(_amount);
}
/*
public, view, onlyOwner와 같은 함수의 키워드 순서는 상관없다.
*/
function renounceOwnership() public view override(Ownable) onlyOwner {
revert("Can't renouce ownership here.");
}
receive() external payable {
emit MoneyReceive(msg.sender, msg.value);
}
fallback () external {
}
}
// solidity-ethereum-bootcamp with Thomas Wiesner
pragma solidity 0.8.0;
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/math/SafeMath.sol";
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/access/Ownable.sol";
contract Allowance is Ownable {
using SafeMath for uint;
event AllwanceChanged(address indexed _forWho, address indexed _fromWhom, uint _oldAmount, uint _newAmount);
mapping(address => uint) public allowance;
function isOwner() internal view returns(bool) {
return owner() == _msgSender();
}
function getBalance() public view returns(uint) {
return address(this).balance;
}
function addAllowance(address _who, uint _amount) public onlyOwner {
emit AllwanceChanged(_who, msg.sender, allowance[_who], _amount);
allowance[_who] = _amount;
}
modifier ownerOrAllowed(uint _amount) {
require(isOwner() || allowance[msg.sender] >= _amount, "You are not allowed");
_;
}
function reduceAllowance(address _who, uint _amount) internal {
emit AllwanceChanged(_who, msg.sender, allowance[_who], allowance[_who] - _amount);
allowance[_who] = allowance[_who].sub(_amount);
}
}
강의에서 제공되는 코드를 하나하나 따라가다보니, "지갑 스마트 컨트랙트를 만든다는 것은 이 지갑을 이용하는 사람마다 스마트 컨트랙트가 배포된다"라는 점을 기본으로 하고 있었다. 출금하고자 하는 금액을 스마트 컨트랙트 내의 잔고와 비교하는 것을 보고 깨달았다. 스마트 컨트랙트 내 입금된 금액을 따로 관리해야하는 줄 알았는데, adderss(this).balance를 통해 관리하면 되었다.
그동안 강의를 수강하면서 나름 많은 예시 코드를 따라하고 있었지만, 계속 와닿지 않던 이유가 앞서 굵은 글씨로 표기한 내용을 이해하지 못 해서 그런 것 같았다. 이번에 주어진 과제를 스스로 해보고, 강사님이 제공해주신 코드와 비교해보면서 번뜩 이해하지 못 하던 것이 무엇인지 깨달아서 다행이다.
[
{
"from": "0xa42b1378D1A84b153eB3e3838aE62870A67a40EA",
"topic": "0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0",
"event": "OwnershipTransferred",
"args": {
"0": "0x0000000000000000000000000000000000000000",
"1": "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4",
"previousOwner": "0x0000000000000000000000000000000000000000",
"newOwner": "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4"
}
}
]
앞으로 스마트 컨트랙트와 웹 서로 상호작용하는 어플리케이션을 만들어나갈텐데, 무엇을 기반으로 상호작용하는지 그동안 잘 이해가 안 되었다. Event를 활용해서 상호작용한다고 했는데, 왜 Event를 작성해서 이런 log를 출력하는지가 와닿지 않았었다.
이번에 지갑 스마트 컨트랙트를 만들면서 다양한 Event를 출력해보니, 트랜잭션 사이트에서 log에 대한 이런 JSON을 긁어올 수 있을 것 같았다. 즉 웹 입장에서 스마트 컨트랙트 내부에서 무슨 일이 일어났는지 알 수 있을 것 같았다.
다음에는 실제로 이러한 상호작용을 위한 웹을 만들어보는 단계이다. 웹 프로그래밍은 너무 오랜만이라 잘 할 수 있을지 모르겠다 😂 그동안 Flutter만 주구장창 해와서 HTML이랑 CSS은 대부분 잊었다. 많이 해본 적도 없긴 하지만... 그렇다고 가만히 있을 수는 없으니 달려보자!
달리는 치타님 제 지갑은 왜안채워지나요