안녕하세요. 스마트 컨트렉트 개발자 개발이 체질의 최원혁입니다.
DeFi Series로 소개해드릴 내용은 BuildBear에서 제공하는 Staking Contract에 대해 알아보겠습니다.
About BuildBear
BuildBear는 개발자가 자신의 테스트 노드를 생성하고 테스트 네트워크를 구축할 수 있는 플랫폼입니다.
이 플랫폼은 팀 전체와 함께 테스트를 쉽게 수행하고, 복잡한 블록체인 트랜잭션을 수행할 때 내부에서 어떤 일이 발생하는지 이해할 수 있게 도와주는 Smart Contract Develop Tool을 제공합니다.
특히 NFT, ICO, Staking, Factory, DEX 등 다양한 Web3 콘텐츠를 탬플릿으로 제공하여 가상 시나리오를 통해 테스트를 지원합니다.
BuildBear is a platform where the developer can create his / her own node and customize it according to the requirements.
This platform provides you with the ease to perform testing at scale and with your entire team and keep understand what happens under the hood when you do your complicated blockchain transactions.
- BuildBear Application : https://home.buildbear.io/
- BuildBear Docs : https://buildbear.notion.site/buildbear/BuildBear-Wiki-7ebf492288b44e0d84ee12d676d462b4
- BuildBear Github : https://github.com/BuildBearLabs
About BuildBear Staking Smart Contract
BuildBear에서 제공하는 Staking Smart Contract의 난이도는 굉장히 낮지만, Solidity로 Staking Reward 로직 구현하고 이해하는데 아주 직관직이고 쉽습니다.로직은 단순히 락업기간에 따라 수익률이 정해져 있고, 유저는 원하는 락업기간을 선택하고 정해진 ETH를 전송합니다. 그리고 락업기간이 지날 경우 원금과 기간에 따른 수익률을 보상으로 받게 되고, 락업 기간 전에 인출할 경우 원금만 돌려받는 간단한 로직입니다. 이런 방식의 Staking Contract는 고정 비율 스테이킹 컨트렉트(Fixed Rate Staking Contract) 라고 불립니다.
지금부터 알아볼 로직은 고정비율 스테이킹 컨트렉트에 적합한 로직이라고 생각하여,
DeFi Smart Contract 개발자 입문용으로 괜찮을것 같아, 이번 게시글에서 공유하려고 합니다.
User Staking Scenario
- 유저는 정해진 기간 동한 ETH를 Staking 하게 되면 수익률에 따라 보상을 받습니다.
- 기간에 따라 수익률이 다르며, LockUp 기간이 길어질수록 수익률 또한 높아집니다.
- 정해진 락업 기간이 지나기 전에 unStaking 하게 되면 원금만 돌려받게 됩니다.
Code Review
1. 상태 변수
address public owner;
// amt of ethers staked by an address
struct Position {
uint positionId;
address walletAddress;
uint createdDate;
uint unlockDate;
uint percentInterest;
uint weiStaked;
uint weiInterest;
bool open;
}
Position position;
uint public currentPositionId;
mapping (uint => Position) public positions;
mapping (address => uint[]) public positionIdsByAddress;
mapping (uint => uint) public tiers;
uint[] public lockPeriods; // 30 days / 60 days
- address public owner : Contract 최초 베포자 주소
- Struct Posotion : 유저가 ETH를 Staking 할 때 Contract에 저장되는 데이터입니다.
- uint positionId : 유저가 Staking 한 정보에 부여된 Id
- address walletAddress : 유저의 지갑 주소
- uint createdDate : 유저가 Staking 한 시간
- uint unlockDate : 유저의 Staking 락업이 풀리는 시간
- uint percentInterest : 유저가 받게 되는 수익률(%)
- uint weiStaked : 유저가 Staking 한 금액
- uint weiInterest : 유저가 받게 될 보상 금액
- bool open : 현재 상태 - uint public currentPositionId : 현재 유저에게 등록된 Staking Posotion ID
- mapping (uint => Position) public positions : Staking Position Id에 대한 Staking 된 정보
- mapping (address => uint[]) public positionIdsByAddress : 유저가 지금까지 Staking 한 Position Id 리스트
- mapping (uint => uint) public tiers : Staking 보상 등급
- uint[] public lockPeriods : Staking Period
2. Constructor init
constructor () payable {
owner = msg.sender;
currentPositionId = 0;
tiers[30] = 700; // 30 days -> 7%
tiers[90] = 1000; // 90 days -> 10%
tiers[180] = 1200; // 180 days -> 12%
lockPeriods.push(30);
lockPeriods.push(90);
lockPeriods.push(180);
}
Lock Up Period & Revanue
- 30 Days = 연간 7% 수익률
- 90 Days = 연간 10% 수익률
- 180 Days = 연간 12% 수익률
3. Function Logic
// num of days staked for
function stakeEther(uint numDays) external payable {
require(tiers[numDays] > 0, "Mapping not found");
positions[currentPositionId] = Position(
currentPositionId,
msg.sender,
block.timestamp,
block.timestamp + (numDays * 15180), // maybe mistake
tiers[numDays],
msg.value,
calculateInterest(tiers[numDays], msg.value),
true
);
positionIdsByAddress[msg.sender].push(currentPositionId);
currentPositionId += 1;
}
function calculateInterest(uint basisPoints, uint weiAmount) private pure returns (uint) {
return basisPoints * weiAmount / 10000;
}
function modifyLockPeriods(uint numDays, uint basisPoints) external {
require(owner == msg.sender, "Only owner may modify staking periods");
tiers[numDays] = basisPoints;
lockPeriods.push(numDays);
}
function getLockPeriods() external view returns(uint[] memory) {
return lockPeriods;
}
function getInterestRate(uint numDays) external view returns (uint) {
return tiers[numDays];
}
function getPositionById(uint positionId) external view returns (Position memory) {
return positions[positionId];
}
function getPositionIdsForAddress(address walletAddress) external view returns (uint[] memory) {
return positionIdsByAddress[walletAddress];
}
function closePosition(uint positionId) external {
require(positions[positionId].walletAddress == msg.sender, "Only position creator can modify the position");
require(positions[positionId].open == true, "Position is closed");
positions[positionId].open = false;
if(block.timestamp > positions[positionId].unlockDate) {
uint amount = positions[positionId].weiStaked + positions[positionId].weiInterest;
payable(msg.sender).call{value: amount}("");
} else {
payable(msg.sender).call{value: positions[positionId].weiStaked}("");
}
}
- stakeEther() : 유저는 stakeEther함수를 실행하는 동시에 Staking 할 ETH를 같이 보냅니다.
그리고 positions[currentPositionId] 변수에 Staking에 대한 정보를 저장합니다. - calculateInterest() : 유저의 락업 기간에 따른 보상을 계산합니다.
- modifyLockPeriods() : Contract owner로 하여금 락업 기간에 따른 수익률(%)을 설정할 수 있습니다.
- getPositionById() :PositionId에 저장된 Staking 정보를 조회합니다.
- getPositionByAddresses() : 유저의 주소로 Contract에 Staking 된 PositionId를 전부 조회합니다.
- closePosition() : Staking된 자금을 인출합니다.
만약 정해진 락업기간이 지났다면, 기간에 맞는 수익률을 보상으로 받습니다. 하지만 락업기간 전에 인출할 경우, 락업된 금액만 돌려받습니다.
4. 마무리
전체 코드 : https://github.com/BuildBearLabs/Tutorials/blob/main/Staking-with-BuildBear/contracts/Staking.sol
Staking Contract의 콘텐츠는 아주 다양합니다. 그중 위와 같이 고정된 수익률을 제공하는 Staking Contract도 디파이 프로토콜로 많이 이용합니다. 고정된 수익률을 제공하는 Staking Contract으로 아주 적절한 예시라고 합니다.
위 컨트렉트는 사실 owner 외에 보안설정이 전혀 없습니다. 물론 Audit을 받거나 상업적으로 사용되는 코드가 아니라 시나리오 용으로 사용하기에 상관없지만, 만약 본인의 프로젝트에 레퍼런스로 사용할 경우 재진입 공격, Access Control 같은 보안체계는 필요해 보입니다.
지금까지 BuildBear Staking Smart Contract의 고정 수익률 로직에 대한 코드를 알아봤습니다.
감사합니다.
댓글