Decentralized Staking App

김현학·2024년 6월 25일
0

speedrun-ethereum

목록 보기
3/5
post-thumbnail

챌린지를 수행하며 기억에 남은 내용만 간단히 회고한다.

Solidity

Smart Contract

Solidity 사용이 처음이었으므로 다음과 같이 velog에 기록해두고 참고하며 계약을 작성했다.

https://velog.io/@oomia/Solidity

최대한 다양한 구성 요소를 활용하여 깔끔하게 작성하려 노력했다. 그 결과는 다음과 같다.

https://gist.github.com/ooMia/99f6c91e3ddd6d0f313806e17dc809fb

Contract Look-up

  1. 외부에서 접근할 때에만 사용되는 함수는 interface에 정의
    • interface 내 모든 함수는 external이어야만 한다
    • external 함수는 내부에서 호출할 수 없다.
interface IStaker {
	// +---------------------+
	// | Function (external) |
	// +---------------------+

	// Receives eth and calls stake()
	receive() external payable;

	// After some `deadline` allow anyone to call an `execute()` function, just once
	// If the deadline has passed and the threshold is met
	// It should call `exampleExternalContract.complete{value: address(this).balance}()`
	function execute() external;

	// If the deadline has passed and the `threshold` was not met,
	// allow everyone to call a `withdraw()` function to withdraw their balance
	function withdraw() external;
}
  1. 외부와 내부에서 모두 사용하는 함수는 public으로 선언하고, abstract에 정의
    • 구현은 실제 contract에서 진행하기 위해 모두 virtual로 정의
    • event도 정의할 수 있었지만, 반드시 상속시킬만한 이유가 없어 구현으로 넘김
    • 추가로 is IStaker을 통해 인터페이스 상속
abstract contract _Staker is IStaker {
	// +-------------------+
	// | Function (public) |
	// +-------------------+

	// Collect funds in a payable `stake()` function and track individual `balances` with a mapping:
	// Make sure to emit `Stake(address,uint256)` event for the frontend `All Stakings` tab to display
	function stake() public payable virtual;

	// Add a `timeLeft()` view function that returns the time left before the deadline for the frontend
	function timeLeft() public view virtual returns (uint256);
}
  1. 기록해둔 내용을 바탕으로 계약 내 순서를 조정했다. 공유되어야 하는 상태 변수들로 시작해서, 마지막에 함수를 배치한다.
    • 일반적으로 assertion을 수행하기 위해 require 문을 사용하고, 원인에 대해서는 문자열을 사용한다.
    • 공통적으로 사용되는 원인들에 대해서만 error를 정의하고 if ... revert <error> 문으로 대체했다.
contract Staker is _Staker {
	// +----------------+
	// | State Variable |
	// +----------------+

	ExampleExternalContract public exampleExternalContract;

	mapping(address => uint256) public balances;
	uint256 public threshold;
	uint256 public deadline;

	bool private openForWithdraw;

	// +-------+
	// | Event |
	// +-------+

	// Make sure to add a `Stake(address,uint256)` event and emit it for the frontend `All Stakings` tab to display)
	event Stake(address indexed staker, uint256 amount);

	// +-------+
	// | Error |
	// +-------+

	error ShouldStakeMoreThanZero();

	// +----------+
	// | Modifier |
	// +----------+

	modifier onProceed() {
		require(!exampleExternalContract.completed(), "Staking completed");
		_;
	}

	modifier onTimeOut() {
		require(isTimeOut(), "Wait for the contract to complete");
		_;
	}
  1. 위에 이어서 함수 구현 내용을 첨부한다. 공통적으로 사용되는 제한 조건에 대해서는 modifier를 사용한다.
    • 명세에는 존재하지 않으나, 조건문에 자주 사용되는 isXX 류의 함수는 별도의 private view로 정의하여 가독성을 높였다.
    • private하지 않은 모든 함수는 override로 재정의해야 한다.
	// +-----------------------+
	// | Function (implements) |
	// +-----------------------+

	constructor(address exampleExternalContractAddress) {
		exampleExternalContract = ExampleExternalContract(
			exampleExternalContractAddress
		);
		threshold = 0.0011 ether;
		deadline = block.timestamp + 5 minutes;
	}

	receive() external payable override {
		stake();
	}

	function execute() external override onProceed onTimeOut {
		if (!openForWithdraw && isThresholdMet()) {
			exampleExternalContract.complete{ value: address(this).balance }();
		}
		openForWithdraw = true;
	}

	function withdraw() external override onTimeOut {
		require(openForWithdraw, "Run Execute first");
		uint256 amount = balances[msg.sender];
		if (amount <= 0) revert ShouldStakeMoreThanZero();
		balances[msg.sender] = 0;
		payable(msg.sender).transfer(amount);
	}

	function stake() public payable override onProceed {
		if (msg.value <= 0) revert ShouldStakeMoreThanZero();
		balances[msg.sender] += msg.value;
		emit Stake(msg.sender, msg.value);
	}

	function timeLeft() public view override returns (uint256) {
		if (block.timestamp >= deadline) {
			return 0;
		}
		return deadline - block.timestamp;
	}

	// +--------------------+
	// | Function (private) |
	// +--------------------+

	function isTimeOut() private view returns (bool) {
		return timeLeft() == 0;
	}

	function isThresholdMet() private view returns (bool) {
		return address(this).balance >= threshold;
	}
}

Import

배포 중 문제 하나는 결과적으로 etherscan에 검증(verified)된 상태로 계약이 배포되어야 한다는 것이다. verify & push라는 웹 사이트 기능을 활용하여 수동으로 작업해도 되지만, 라이브러리나 계약 작성에 다양한 계약들을 참조하면 사용하기 어려운 기능이다.

hardhat 환경에서는 이를 편리하게 해결할 수 있다.

https://hardhat.org/hardhat-runner/plugins/nomicfoundation-hardhat-verify#usage

npx hardhat verify --network mainnet DEPLOYED_CONTRACT_ADDRESS "Constructor argument 1"

일반적인 환경에서의 문법은 위와 같으며, 본 프로젝트에서는 다음과 같이 정의되어 있다.

yarn hardhat-verify

0개의 댓글