7-2. TinyBank) 입금(withdraw) & 보상(reward) 기능

동동주·2025년 10월 19일


0. 입금(withdraw) 기능
1. 보상 로직 (비효율ver.)
2. 보상 로직 (권장)
3. git commit


0. 입금(withdraw) 기능

어렵지 않아서 설명은 생략.. 나중에 봐도 충분히 이해할 것 같다

contract/TinyBank.sol 코드

event Withdraw(uint256 amount, address to);

.
.
.

function withdraw(uint256 _amount) external {
        require(staked[msg.sender] >= _amount, "insufficient staked token" );
        stakingToken.transfer(_amount, msg.sender);
        staked[msg.sender] -= _amount;
        totalStaked -= _amount;
        emit Withdraw(_amount, msg.sender);
    }

test/TinyBank.ts 코드

마지막 코드 줄 제외하고 it 내부가 Staking 부분과 동일하다.

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 tinyBankC.withdraw(stakingAmount);
      expect(await tinyBankC.staked(signer0.address)).equal(0);
    });
  });



1. 보상 로직 (비효율ver.)

해당 코드대로면 사용자가 많을수록 매번 계산이 기하급수적으로 많아져서 사실상 불가능한 코드.... 그치만 대충 원리 이해하라고 알려주신 것 같다.

보상 개념 (공통)

보상을 왜 주는지 자체는.. 간단하게 적으면
보상이 없으면 그냥 내 지갑에 넣어두지 굳이 staking(예치)할 필요 없으니까..그렇기도 하고, 애초에 보상 자체가 블록 생성의 동력인 것이 PoS니까 당연히 보상 로직이 있어야겠지.. 라고 받아들임
--> 7-1. TinyBank) 기본구조 & Stake 기능 PoS, Staking 개념 참고

  • TinyBank의 구조
    Reward
    reward token : MyToken
    reward resources : 1 MT/block minting
    reward strategy : staked[user]/totalStaked distribution

위에 적었듯 보상은 그냥 1MT를 만들어서(minting) 지분만큼 줄 것이다.

contract/MyToken.sol 코드

근데 기존에 있는 _mint 함수는 internal용이다. (호출불가)
이렇게 사용하면 정말정말 위험하지만.. external로 mint를 선언해줌
(다음에 modifier 배우고 수정한다고 함.) --> 8-2. mint 함수 접근권한 제한하기 (access control)

위험한 이유는.. 저 기능을 알기만 하면 누구나 코인을 찍어낼 수 있기 때문..

function mint(uint256 amount, address owner) external {
        _mint(amount, owner);
    } //modifier 배우고 수정 예정 (현재 매우 위험한 코드)

contract/TinyBank.sol 코드

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

interface IMyToken {
    // MyToken.sol에서 사용할 함수의 헤더 가져오기
    function transfer(uint256 amount, address to) external;
    function transferFrom(address from, address to, uint256 amount) external;
    function mint(uint256 amount, address owner) external;
}



contract TinyBank {
    event Staked(address from, uint256 amount);
    event Withdraw(uint256 amount, address to);

    IMyToken public stakingToken; 

    mapping(address => uint256) public lastClaimedBlock; //reawrd 기준점.
    address[] public stakedUsers;
    //mapping의 특징 : 키값을 모두 가져올 수 없음 (solidity 한정)
    //크기 지정 안함 = dynamic array
    uint256 rewardPerBlock = 1*10 ** 18;

    mapping(address => uint256) public staked; 
    uint256 public totalStaked; 

    

    constructor(IMyToken _stakingToken) { 
        stakingToken = _stakingToken;
    }


    function stake(uint256 _amount) external {
        require(_amount >= 0, "cannot stake 0 amount");
        
        stakingToken.transferFrom(msg.sender, address(this), _amount);
        staked[msg.sender] += _amount;
        totalStaked += _amount;
        stakedUsers.push(msg.sender); //
        emit Staked(msg.sender, _amount);
    }

    function withdraw(uint256 _amount) external {
        require(staked[msg.sender] >= _amount, "insufficient staked token" );
        distributeReward();
        stakingToken.transfer(_amount, msg.sender);
        staked[msg.sender] -= _amount;
        totalStaked -= _amount;
        // 10만명이 쓴다면...?
        if (staked[msg.sender] == 0 ) {
            uint256 index;
            for (uint i = 0; i < stakedUsers.length; i++) {
                if (stakedUsers[i] == msg.sender) {
                    index = i;
                    break;
                }
            }
            stakedUsers[index] = stakedUsers[stakedUsers.length -1];
            stakedUsers.pop();
        }
        emit Withdraw(_amount, msg.sender);
    }


// transaction... (값이 변하니까)
// who, when?
    function distributeReward() internal {
        for (uint i = 0; i < stakedUsers.length; i++) {
            uint256 blocks = block.number - lastClaimedBlock[stakedUsers[i]]
            uint256 reward = blocks * rewardPerBlock * staked[stakedUsers[i]] / totalStaked ;
            stakingToken.mint(reward, stakedUsers[i]);
            lastClaimedBlock[stakedUsers[i]] = block.number;
            
            }
    }

}

코드 흐름

  • 아까 만든 외부호출 가능한 mint 함수 헤더 가져오기

  • lastClaimedBlock으로 보상 기준블록 잡기
    유저마다 staking을 한 시점이 다르고, 다르게 보상을 줘야하니까 lastClaimedBlock 으로 보상의 기준점 잡기.

  • stakedUsers라는 주소 배열 만들어서 저장
    stake()에서 push메서드로 배열이 추가되고,
    withdraw()에서 staked(예치해둔 돈) == 0이면
    배열을 처음부터 끝까지 for문을 돌려서 index를 찾고 제거함.
    + (solidity의 mapping은 키값을 모두 가져올 수 없기에 따로 배열을 만들었다고 한 것 같음)

  • distributeReward() 보상 분배/지급 함수
    lastClaimedBlock과 현재블록(block.number)을 비교하고 전체 보상에서 해당 사용자의 지분 계산을 통해서 보상을 주도록 만들어짐. (블록 수 × 블록당 보상량 × 내 점유율) 그리고 해당 보상은 withdraw() 내부에 들어가서 출금 시 보상이 주어짐.
    + distributeReward()은 값을 변경하는 transaction이라 호출 시 가스비가 소모됨. = 그래서 누가 언제 호출할건지가 중요하다..?



2. 보상 로직 (권장)

해당 코드 역시 완벽한 건 아니고... 보상이 공평하지 않다는 단점이 있다.
함수 호출 시점에서의 지분으로 분배하는 코드라, 호출 시점에 따른 불이익이 있을 수 있다. 나 혼자서만 계속 예치되어 있다가 보상함수 호출 직전에 다른 사람이 들어와도, 누적된 모든 블록에 대한 나의 보상 지분도 줄어드는 형식이다.
하지만 공평한 알고리즘을 만들기엔 수업 시간이 길어져서 강의에서 다루지 않고 넘어갔다. 나중에 찾아봐야지...

contract/TinyBank.sol 코드

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

interface IMyToken {
    // MyToken.sol에서 사용할 함수의 헤더 가져오기
    function transfer(uint256 amount, address to) external;
    function transferFrom(address from, address to, uint256 amount) external;
    function mint(uint256 amount, address owner) external;
}



contract TinyBank {
    event Staked(address from, uint256 amount);
    event Withdraw(uint256 amount, address to);

    IMyToken public stakingToken; 

    mapping(address => uint256) public lastClaimedBlock; //reawrd 기준점.
    uint256 rewardPerBlock = 1*10 ** 18;

    mapping(address => uint256) public staked; 
    uint256 public totalStaked; 

    

    constructor(IMyToken _stakingToken) { 
        stakingToken = _stakingToken;
    }


    function stake(uint256 _amount) external {
        require(_amount >= 0, "cannot stake 0 amount");
        distributeReward(msg.sender);
        stakingToken.transferFrom(msg.sender, address(this), _amount);
        staked[msg.sender] += _amount;
        totalStaked += _amount;
        emit Staked(msg.sender, _amount);
    }

    function withdraw(uint256 _amount) external {
        require(staked[msg.sender] >= _amount, "insufficient staked token" );
        distributeReward(msg.sender);
        stakingToken.transfer(_amount, msg.sender);
        staked[msg.sender] -= _amount;
        totalStaked -= _amount;
        emit Withdraw(_amount, msg.sender);
    }


    function distributeReward(address to) internal {
        uint256 blocks = block.number - lastClaimedBlock[to];
        uint256 reward = (blocks * rewardPerBlock * staked[to]) / totalStaked ;
        stakingToken.mint(reward, to);
        lastClaimedBlock[to] = block.number;
    } //공평한 분배는 아님. 하지만 다루지 않음.

}

코드 흐름

  • 아까 만든 stakedUsers라는 주소 배열 없애기
    여기에 연결된 for문, push같은 코드들도 없애줘야 한다.

  • 함수를 호출한 사람(=to)에 대해서만 보상 계산
    보상 함수에서 주소를 받아 그 사람이 받을 보상을 계산하고 지급한다.
    주소는 매개변수에서 to로 받아오고, 기존에 stakedUsers[i] 로 들어있던 부분을 to로 바꿔주면 된다.



3. git commit

- add mint(external)

❯ git add contracts/MyToken.sol 
❯ git commit -m "add mint(external)"

- add reward logic

❯ git add contracts/TinyBank.sol 
❯ git commit -m "add reward logic" 



테스트는 다음주차에...

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

0개의 댓글