0. 입금(withdraw) 기능
1. 보상 로직 (비효율ver.)
2. 보상 로직 (권장)
3. git commit
어렵지 않아서 설명은 생략.. 나중에 봐도 충분히 이해할 것 같다
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);
}
마지막 코드 줄 제외하고 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);
});
});
해당 코드대로면 사용자가 많을수록 매번 계산이 기하급수적으로 많아져서 사실상 불가능한 코드.... 그치만 대충 원리 이해하라고 알려주신 것 같다.
보상을 왜 주는지 자체는.. 간단하게 적으면
보상이 없으면 그냥 내 지갑에 넣어두지 굳이 staking(예치)할 필요 없으니까..그렇기도 하고, 애초에 보상 자체가 블록 생성의 동력인 것이 PoS니까 당연히 보상 로직이 있어야겠지.. 라고 받아들임
--> 7-1. TinyBank) 기본구조 & Stake 기능 PoS, Staking 개념 참고
위에 적었듯 보상은 그냥 1MT를 만들어서(minting) 지분만큼 줄 것이다.
근데 기존에 있는 _mint 함수는 internal용이다. (호출불가)
이렇게 사용하면 정말정말 위험하지만.. external로 mint를 선언해줌
(다음에 modifier 배우고 수정한다고 함.) --> 8-2. mint 함수 접근권한 제한하기 (access control)
위험한 이유는.. 저 기능을 알기만 하면 누구나 코인을 찍어낼 수 있기 때문..
function mint(uint256 amount, address owner) external {
_mint(amount, owner);
} //modifier 배우고 수정 예정 (현재 매우 위험한 코드)
// 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은 키값을 모두 가져올 수 없기에 따로 배열을 만들었다고 한 것 같음)
해당 코드 역시 완벽한 건 아니고... 보상이 공평하지 않다는 단점이 있다.
함수 호출 시점에서의 지분으로 분배하는 코드라, 호출 시점에 따른 불이익이 있을 수 있다. 나 혼자서만 계속 예치되어 있다가 보상함수 호출 직전에 다른 사람이 들어와도, 누적된 모든 블록에 대한 나의 보상 지분도 줄어드는 형식이다.
하지만 공평한 알고리즘을 만들기엔 수업 시간이 길어져서 강의에서 다루지 않고 넘어갔다. 나중에 찾아봐야지...
// 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로 바꿔주면 된다.
❯ git add contracts/MyToken.sol
❯ git commit -m "add mint(external)"
❯ git add contracts/TinyBank.sol
❯ git commit -m "add reward logic"
테스트는 다음주차에...