12-2. TinyBank.vy 완성

동동주·2025년 11월 19일
post-thumbnail


Todo :
0. 배포 확인
1. Stake & Withdraw 기능 구현
2. 이벤트 구현
3. reawrd & rewardPerBlock 변경 기능 구현
3-1. Decentralized access control (분산형 접근 제어)로 변경




contracts/TinyBank.vy (0~1)

# @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

0. 배포 확인

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] 매개변수 추가해줌

TinyBank.sol 비교

constructor(IMyToken _stakingToken, address[MANAGER_NUMBERS] memory _managers) MultiManagedAccess(msg.sender, _managers) { 
        stakingToken = _stakingToken;
        rewardPerBlock = defaultRewardPerBlock;
    }

배포확인 시 2개 통과


1. Stake & Withdraw 기능 구현

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

입출금 기능 시 총 4개 통과

+@) 여담으로 저 interface 안에 함수 불러오면서
함수 매개변수 순서 이야기하시면서 api 이야기를 하셨다
(api를 잘 설정해놔야 파일 확인하면서 왔다갔다 하지 않는다고...)
그래서 api랑 비교하면서 좀 더 알아봐야겠다 더찾아보기, 추후링크첨부예정




2. 이벤트 구현

test/TinyBank.ts

  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);

contracts/TinyBank.vy

... 

#이벤트 추가
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개가 통과한다.
입출금 기능 시 총 4개 통과




3. reawrd & rewardPerBlock 변경 기능 구현

contracts/TinyBank.vy (구버전)

아래 첨언하겠지만 분산형 관리자가 아닌 이전 테스트 버전에서만 모든 테스트를 충족한다. ( 아마도 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 추가하고 생성자에서 할당
    (분산형 접근 설정에 맞추느라 편집 예정)

reward 기능 5개 통과~!

교수님은 위의 코드처럼 하셨고 이러면 기존 test는 다 통과된다.
다만,
9-1. Decentralized access control (분산형 접근 제어)
이 단계에서 분산형 접근 제어를 하면서 추가해놓은 것 때문에
나는 그냥 통과가 안되고 통과하는 테스트 개수가
위의 사진처럼 기본 reward 기능 1개를 추가한 5개에서 변하지 않는다...
그래서 아래에서 수정해보았다.

  • 최신 버전? 에서는 deploy로 해야한다고...
    hardhat vyper 에서는 모듈화가 안된다... 직접 vyper compiler로 해야하고, 문법도 지금까지 사용한 것과 다름.


3-1. Decentralized access control (분산형 접근 제어)로 변경

Solidity 버전 참고 : 9-1. Decentralized access control (분산형 접근 제어)


contracts/TinyBank.vy

# @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에 배포하고 테스트해볼 예정..?!

profile
배운 내용 정리&기록, 스크랩

0개의 댓글