
Todo :
0. 배포 확인
1. Stake & Withdraw 기능 구현
2. 이벤트 구현
3. reawrd & rewardPerBlock 변경 기능 구현
3-1. Decentralized access control (분산형 접근 제어)로 변경
# @version ^0.3.0
# @license MIT
interface IMyToken:
def transfer(_amount:uint256, _to:address): nonpayable
def transferFrom(_owner:address, _to:address, _amount:uint256): nonpayable
def mint(_amount:uint256, _to:address): nonpayable
staked: public(HashMap[address, uint256])
totalStaked: public(uint256)
stakingToken:IMyToken
MANAGER_NUMBERS : constant(int128) = 5
@external
def __init__(_stakingToken:IMyToken, _managers: address[MANAGER_NUMBERS]):
self.stakingToken = _stakingToken
@external
def stake(_amount: uint256):
assert _amount > 0, "cannot stake 0 amount"
self.stakingToken.transferFrom(msg.sender, self, _amount)
self.staked[msg.sender] += _amount
self.totalStaked += _amount
@external
def withdraw(_amount: uint256):
assert self.staked[msg.sender] >= _amount, "insufficient staked token"
self.stakingToken.transfer(_amount, msg.sender)
self.staked[msg.sender] -= _amount
self.totalStaked -= _amount
interface IMyToken:
def name(): nonpayable
nonpayable
찾아보니 기본값으로 nonpayable이 되어있는 것 같긴 하다.
주석처리 해보고 test 돌려보면 컴파일 오류는 없긴 하다...?
참고 : https://docs.vyperlang.org/en/stable/control-structures.html
생성자 매개변수 설정
교수님은 _stakingToken:IMyToken만 적으셨는데,
저번에 매니저 modifier 설정하느라 매개변수에 매니저들도 받는 걸로 해놔서.. 그냥 저것만 넣으면 오류가 나서 배포가 안되었다...
그래서 일단 MANAGER_NUMBERS : constant(int128) = 5 로 상수 설정해주고 생성자에 _managers: address[MANAGER_NUMBERS] 매개변수 추가해줌
constructor(IMyToken _stakingToken, address[MANAGER_NUMBERS] memory _managers) MultiManagedAccess(msg.sender, _managers) {
stakingToken = _stakingToken;
rewardPerBlock = defaultRewardPerBlock;
}

MyBank.vy에서 했던 것과 유사하게 기능을 구현해준다.
참고 : 12-1. MyToken.vy 완성

+@) 여담으로 저 interface 안에 함수 불러오면서
함수 매개변수 순서 이야기하시면서 api 이야기를 하셨다
(api를 잘 설정해놔야 파일 확인하면서 왔다갔다 하지 않는다고...)
그래서 api랑 비교하면서 좀 더 알아봐야겠다 더찾아보기, 추후링크첨부예정
describe("Staking", async () => {
it("should return staked amount", async () => {
const signer0 = signers[0];
const stakingAmount = hre.ethers.parseUnits("50", DECIMALS);
await myTokenC.approve(await tinyBankC.getAddress(), stakingAmount);
//이벤트 확인
await expect(tinyBankC.stake(stakingAmount))
.to.emit(tinyBankC, "Staked")
.withArgs(signer0.address, stakingAmount);
expect(await tinyBankC.staked(signer0.address)).equal(stakingAmount);
expect(await tinyBankC.totalStaked()).equal(stakingAmount);
expect(await myTokenC.balanceOf(tinyBankC)).equal(
await tinyBankC.totalStaked(),
);
});
});
describe("Withdraw", async () => {
it("should return 0 staked after withdrawing total token", async () => {
const signer0 = signers[0];
const stakingAmount = hre.ethers.parseUnits("50", DECIMALS);
await myTokenC.approve(await tinyBankC.getAddress(), stakingAmount);
await tinyBankC.stake(stakingAmount);
//이벤트 확인
await expect(tinyBankC.withdraw(stakingAmount))
.to.emit(tinyBankC, "Withdraw")
.withArgs(stakingAmount, signer0.address);
expect(await tinyBankC.staked(signer0.address)).equal(0);
});
});
이벤트 발생을 확인하는 테스트 코드로 변경해주고 (기존에는 expect 없이 아래와 같은 코드)
await tinyBankC.withdraw(stakingAmount);
...
#이벤트 추가
event Staked:
_owner: indexed(address)
_amount: uint256
event Withdraw:
_amount: uint256
_to: indexed(address)
...
...
@external
def stake(_amount: uint256):
assert _amount > 0, "cannot stake 0 amount"
self.stakingToken.transferFrom(msg.sender, self, _amount)
self.staked[msg.sender] += _amount
self.totalStaked += _amount
#이벤트 로그 추가
log Staked(msg.sender, _amount)
@external
def withdraw(_amount: uint256):
assert self.staked[msg.sender] >= _amount, "insufficient staked token"
self.stakingToken.transfer(_amount, msg.sender)
self.staked[msg.sender] -= _amount
self.totalStaked -= _amount
#이벤트 로그 추가
log Withdraw(_amount, msg.sender)
이벤트를 넣어준다. (역시나 이전 항목 참고 : 12-1. MyToken.vy 완성)
그럼 테스트 코드만 바꿔줬을 땐 배포시처럼 2개만 통과하다가
contract 코드를 위에처럼 바꿔주면 다시 아래 사진처럼 4개가 통과한다.

아래 첨언하겠지만 분산형 관리자가 아닌 이전 테스트 버전에서만 모든 테스트를 충족한다. ( 아마도 8-3. rewardPerBlock 변경 기능 (setRewardPerBlock) 여기 test )
# @version ^0.3.0
# @license MIT
#상수
INIT_REWARD: constant(uint256) = 1 * 10 ** 18
MANAGER_NUMBERS : constant(int128) = 5
interface IMyToken:
def transfer(_amount:uint256, _to:address): nonpayable
def transferFrom(_owner:address, _to:address, _amount:uint256): nonpayable
def mint(_amount:uint256, _to:address): nonpayable
event Staked:
_owner: indexed(address)
_amount: uint256
event Withdraw:
_amount: uint256
_to: indexed(address)
staked: public(HashMap[address, uint256])
totalStaked: public(uint256)
stakingToken:IMyToken
#추가
rewardPerBlock: uint256
lastClaimedBlock: HashMap[address, uint256]
owner: address
manager: address
@external
def __init__(_stakingToken:IMyToken, _managers: address[MANAGER_NUMBERS]):
self.stakingToken = _stakingToken
#추가
self.rewardPerBlock = INIT_REWARD
self.owner = msg.sender
self.manager = msg.sender
@internal
def onlyOwner(_owner: address):
assert self.owner == _owner, "You are not authorized"
@internal
def onlyManager(_manager: address):
assert self.manager == _manager, "You are not authorized to manage this contract"
@external
def setRewardPerBlock(_amount: uint256):
self.onlyManager(msg.sender)
self.rewardPerBlock = _amount
@internal
def updateReward(_to: address):
#staking 한 적이 있는 경우에만 실행
if self.staked[_to] > 0:
blocks: uint256 = block.number - self.lastClaimedBlock[_to]
reward: uint256 = self.rewardPerBlock * blocks * self.staked[_to] / self.totalStaked
self.stakingToken.mint(reward, _to)
self.lastClaimedBlock[_to] = block.number
@external
def stake(_amount: uint256):
assert _amount > 0, "cannot stake 0 amount"
self.updateReward(msg.sender)
self.stakingToken.transferFrom(msg.sender, self, _amount)
self.staked[msg.sender] += _amount
self.totalStaked += _amount
log Staked(msg.sender, _amount)
@external
def withdraw(_amount: uint256):
assert self.staked[msg.sender] >= _amount, "insufficient staked token"
self.updateReward(msg.sender)
self.stakingToken.transfer(_amount, msg.sender)
self.staked[msg.sender] -= _amount
self.totalStaked -= _amount
log Withdraw(_amount, msg.sender)
함수 정의 순서 신경쓰기~!
(코드 위치상)정의 된 이후에만 함수를 사용가능
상수는 변수랑 다르게 위로 올려서 정리해주기
msg.sender 사용은 external에서만 가능하다는 거 잊지 말기
rewardPerBlock, lastClaimedBlock & updateReward 함수
블록 당 리워드를 초기값으로 생성자에서 할당하고, setRewardPerBlock 함수에서 변경하게끔 변수 설정.
updateReward 함수에서 reward를 줄 블록을 계산함. (현재블록-지난번 블록 값) 단, staking 한 적이 없으면 보상 안되게 막아줌. (if self.staked[_to] > 0:) 부분
이 때, 지난번 블록 값을 저장하는 변수로 lastClaimedBlock를 사용한다. 보상 여부와 무관하게 함수 호출 시 현재 블록을 lastClaimedBlock에 저장.
입/출금 시 updateReward 함수 실행
owner, manager 추가하고 생성자에서 할당
(분산형 접근 설정에 맞추느라 편집 예정)

교수님은 위의 코드처럼 하셨고 이러면 기존 test는 다 통과된다.
다만,
9-1. Decentralized access control (분산형 접근 제어)
이 단계에서 분산형 접근 제어를 하면서 추가해놓은 것 때문에
나는 그냥 통과가 안되고 통과하는 테스트 개수가
위의 사진처럼 기본 reward 기능 1개를 추가한 5개에서 변하지 않는다...
그래서 아래에서 수정해보았다.
Solidity 버전 참고 : 9-1. Decentralized access control (분산형 접근 제어)
# @version ^0.3.0
# @license MIT
#상수
INIT_REWARD: constant(uint256) = 1 * 10 ** 18
MANAGER_NUMBERS : constant(int128) = 5
interface IMyToken:
def transfer(_amount:uint256, _to:address): nonpayable
def transferFrom(_owner:address, _to:address, _amount:uint256): nonpayable
def mint(_amount:uint256, _to:address): nonpayable
event Staked:
_owner: indexed(address)
_amount: uint256
event Withdraw:
_amount: uint256
_to: indexed(address)
staked: public(HashMap[address, uint256])
totalStaked: public(uint256)
stakingToken:IMyToken
rewardPerBlock: uint256
lastClaimedBlock: HashMap[address, uint256]
owner: address
## manager: address 대신 추가
confirmed: bool[MANAGER_NUMBERS]
managers: address[MANAGER_NUMBERS]
@external
def __init__(_stakingToken:IMyToken, _managers: address[MANAGER_NUMBERS]):
self.stakingToken = _stakingToken
self.rewardPerBlock = INIT_REWARD
self.owner = msg.sender
## self.manager = msg.sender 대신 추가
for i in range(MANAGER_NUMBERS):
self.managers[i] = _managers[i]
############### modifier 기능 구현 ################
@internal
def onlyOwner(_owner: address):
assert self.owner == _owner, "You are not authorized"
@internal #
def isManager(_manager: address) -> bool:
found : bool = False
for i in range(MANAGER_NUMBERS):
if(self.managers[i] == _manager):
found = True
break
return found
@internal
def managerInfo(_manager: address) -> uint256:
for i in range(MANAGER_NUMBERS):
if(self.managers[i] == _manager):
return i
assert False, "You are not a managers" #return 되지 않았을 때만 실행
return 0 #컴파일러용..
@external
def confirm():
assert self.isManager(msg.sender), "You are not a managers"
self.confirmed[self.managerInfo(msg.sender)] = True
@internal
def allConfirmed() -> bool:
for i in range(MANAGER_NUMBERS):
if not self.confirmed[i]:
return False
return True
@internal
def confirmReset():
for i in range(MANAGER_NUMBERS):
self.confirmed[i] = False
@internal
def onlyAllConfirmed(_sender: address):
assert self.isManager(_sender), "You are not a managers"
assert self.allConfirmed(), "Not all confirmed yet"
self.confirmReset()
#Deprecated....(vyper에는 없음)
#@internal
#def onlyManager(_manager: address):
# assert self.manager == _manager, "You are not authorized to manage this contract"
##################################
@external
def setRewardPerBlock(_amount: uint256):
##self.onlyManager(msg.sender)
##추가!
self.onlyAllConfirmed(msg.sender)
self.rewardPerBlock = _amount
@internal
def updateReward(_to: address):
#staking 한 적이 있는 경우에만 실행
if self.staked[_to] > 0:
blocks: uint256 = block.number - self.lastClaimedBlock[_to]
reward: uint256 = self.rewardPerBlock * blocks * self.staked[_to] / self.totalStaked
self.stakingToken.mint(reward, _to)
self.lastClaimedBlock[_to] = block.number
@external
def stake(_amount: uint256):
assert _amount > 0, "cannot stake 0 amount"
self.updateReward(msg.sender)
self.stakingToken.transferFrom(msg.sender, self, _amount)
self.staked[msg.sender] += _amount
self.totalStaked += _amount
log Staked(msg.sender, _amount)
@external
def withdraw(_amount: uint256):
assert self.staked[msg.sender] >= _amount, "insufficient staked token"
self.updateReward(msg.sender)
self.stakingToken.transfer(_amount, msg.sender)
self.staked[msg.sender] -= _amount
self.totalStaked -= _amount
log Withdraw(_amount, msg.sender)
- MANAGER_NUMBERS 상수 선언
(5로 설정했고, 테스트에서 주소 생성자로 넣어주는 구조)
- confirmed, managers 배열
confirm 여부 (bool)를 저장할 배열 confirmed와
매니저들의 주소 (address)를 저장할 배열 managers 선언
- 생성자에서 managers 할당
for문을 사용해서 매개변수로 입력받은 _managers배열을 managers에 deep copy
- 매니저인지 확인하는 부분 매서드를 따로 만들어줌 (isManager)
modifier onlyAllConfirmed { //매니저가 호출했는지 확인 bool isManager = false; for(uint i=0; i<MANAGER_NUMBERS; i++) { if(managers[i] == msg.sender) { isManager = true; break; } require(isManager, "You are not a managers"); } // 모든 매니저가 동의했는지 확인 require(allConfirmed(),"Not all confirmed yet"); // confirm 기록 초기화 reset(); _; }function confirm() external { bool found = false; for(uint i=0; i<MANAGER_NUMBERS; i++) { if(managers[i] == msg.sender) { found = true; confirmed[i] = true; break; } } require(found, "You are not a managers"); }위의 코드처럼 반복..? 되던 매니저 여부 검사를 그냥 따로 만들어버림
(물론 어떤게 더 적절한지는... 모르겠음)
isManager(_manager: address) -> bool 형태로
msg.sender 받아주고 (internal) 매니저 여부를 반환 (bool)
- managerInfo로 매니저 정보 받아오는 거 메서드로 만들어봄..
이것 역시 적절한건지... 모르겠음 좀 오히려 지저분한 것 같기도 하고..
일단 루프 돌아서 msg.sender가 매니저이면 번호 반환
아닐시(return 안되면 assert 도달) revert 됨
(컴파일러때문에 return 0 작성)
- onlyAllConfirmed 함수로 modifier 기능 구현
isManager(_sender) 반환값으로"You are not a managers"revert 판단.
allConfirmed() 반환값으로"Not all confirmed yet"revert 판단.
다 통과되었다면 리워드를 바꾸는데 동의여부를 사용했으니 confirmReset()로 동의여부 초기화.
⇒ setRewardPerBlock(_amount: uint256) 함수 첫줄에 onlyAllConfirmed(msg.sender) 추가하여 modifier 기능처럼 사용!
- Python/Vyper에서는 False / True (대문자) 사용
소문자 false / true → UndeclaredDefinition 오류가 난다
몰랐다면 일괄 찾기&변경하기로 바꿔야한다.. (귀찮음)
- 반환값이 있는 경우
def 함수명(매개변수) -> 반환값타입 :으로 적어줘야한다
이 때 모든 경우에 반환값이 존재해야하는 점 조심 (컴파일 오류남)
- internal, external
internal에서 external 호출 불가 및 msg.sender 사용 불가!!
- self 사용 잊지말기
변수에 접근할 때, 함수호출시 self 붙여줘야 함
(external/internal 무관하게 internal 함수 호출시 전부 필요함)
- 변수 작성 양식
이름 : 타입 = 값 형식 지키기
- for 문 형식!!
Solidity :for(uint i=0; i<MANAGER_NUMBERS; i++) { ... }
Vyper :for i in range(MANAGER_NUMBERS):
- vyper에는 중괄호 없고, 소괄호도 안쓰는 경우가 많음...!
- ! 대신 not 사용
Solidity :if(!confirmed[i]) { ... }
Vyper :if not self.confirmed[i]:

드디어 문제없이 다 실행된다!!
문법이 낯설어서 헷갈려하느라 오류잡는데 고생했다...
분명 오류가 5개였는데.... 3개에서 1개까지 줄인줄 알았더니
뭔가 잘못이해하고 고쳐서 다시 8개 막 이렇게 널뛰기하는 걸 보면서...... 왜 성공하길 비는지 조금 알 것 같았다..
그래도 고생끝에 성공하니까 너무 즐겁다..!!!!!
무엇보다도 과제나 수업이라서 감점이 없을지, 바꿔도 괜찮은지 신경쓰다가
언급 안하고 끝나서 남은 부분이었어서




그런 거 일절 신경 안쓰고 내 맘대로 만든다는 게 너무너무 재밌었다...
지금까지 만든 contract를 hardhat network에서 배포해보는 게 아니라 진짜 blockchain network에 배포하고 테스트해볼 예정..?!