- NFT 판매자가 이 계약을 배포합니다.
- 경매는 7일 동안 진행됩니다.
- 참가자들은 현재 최고 입찰자보다 많은 ETH를 입금함으로써 입찰할 수 있습니다.
- 최고 입찰자가 아닌 모든 입찰자는 자신의 입찰금을 철회할 수 있습니다.
- 옥션 종료 후 최고 입찰자가 NFT의 새로운 소유자가 됩니다.
- 판매자는 최고 입찰가의 ETH를 받습니다.
Remix: https://remix.ethereum.org/
- 모든 폴더와 파일 삭제
- contracts 폴더와 solidity 파일 생성
lisence: MIT
컴파일 버전: ^0.8.24
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
IERC721의 safeTransferFrom과 transferFrom 함수를 사용할 것입니다.
interface IERC721 {
function safeTransferFrom(address from, address to, uint256 tokenId)
external;
function transferFrom(address, address, uint256) external;
}
EnglishAuction contract 생성
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
interface IERC721 {
function safeTransferFrom(address from, address to, uint256 tokenId)
external;
function transferFrom(address, address, uint256) external;
}
contract EnglishAuction {}
4개의 event를 만들 것입니다.
- Start(): 경매가 시작되었음을 알리는 이벤트.
- Bid(address indexed sender, uint256 amount): 주소 sender에서 금액 amount으로 새로운 입찰이 이루어졌음을 알리는 이벤트.
- Withdraw(address indexed bidder, uint256 amount): 주소 bidder가 입찰 금액 amount을 철회함을 알리는 이벤트.
- End(address winner, uint256 amount): 경매가 종료됨을 알리는 이벤트.
event Start();
event Bid(address indexed sender, uint256 amount);
event Withdraw(address indexed bidder, uint256 amount);
event End(address winner, uint256 amount);
- NFT 관련 변수들
- IERC721 public nft
IERC721 인터페이스 타입의 public 변수. 이 변수는 경매에 사용될 NFT의 인터페이스를 참조합니다. 이를 통해 컨트랙트는 NFT의 소유권 이전과 관련된 함수들을 호출할 수 있습니다.
- uint256 public nftId
경매에 사용될 NFT의 고유 식별자(ID)입니다. 이 ID는 특정 NFT를 구분하고 참조하는 데 사용됩니다.
IERC721 public nft;
uint256 public nftId;
- 옥션 관련 변수들
- address payable public seller
NFT 판매자의 주소를 저장하는 변수입니다. 이 주소는 이더리움 네트워크 상의 지불 가능한 주소 형태로, 경매 종료 후 최고 입찰 금액을 이 주소로 전송합니다.
- uint256 public endAt
경매 종료 시점을 나타내는 타임스탬프입니다. 이 시점 이후에는 더 이상 입찰을 받지 않습니다.
- bool public started
경매가 시작되었는지 여부를 나타내는 변수입니다. 경매 시작 시 true로 설정되고, 그 전까지는 false입니다.
- bool public ended
경매가 종료되었는지 여부를 나타내는 변수입니다. 경매 종료 시 true로 설정됩니다.
- address public highestBidder
현재 최고 입찰자의 주소를 저장하는 변수입니다.
- uint256 public highestBid
현재까지의 최고 입찰 금액을 저장하는 변수입니다.
- mapping(address => uint256) public bids
각 주소에 대해 입찰된 금액을 저장하는 매핑입니다. 이는 입찰자가 입찰한 금액을 관리하고, 나중에 최고 입찰자가 아닌 경우 금액을 회수할 수 있도록 합니다.
address payable public seller;
uint256 public endAt;
bool public started;
bool public ended;
address public highestBidder;
uint256 public highestBid;
mapping(address => uint256) public bids;address payable public seller;
uint256 public endAt;
bool public started;
bool public ended;
address public highestBidder;
uint256 public highestBid;
mapping(address => uint256) public bids;
- 매개변수:
address _nft: 경매에 사용될 NFT를 대표하는 스마트 컨트랙트의 주소입니다.
uint256 _nftId: 경매에 사용될 NFT의 고유 식별자(ID)입니다.
uint256 _startingBid: 경매 시작 시의 최저 입찰 금액입니다.
- 기능:
nft = IERC721(_nft): 주어진 주소 _nft에서 IERC721 인터페이스를 구현하는 컨트랙트를 참조하여, nft 변수에 할당합니다. 이를 통해 경매 컨트랙트는 해당 NFT에 대한 다양한 작업을 수행할 수 있습니다.
nftId = _nftId: 입력받은 NFT의 ID를 nftId 변수에 저장합니다. 이 ID는 특정 NFT를 경매 컨트랙트에서 참조하고 관리하는 데 사용됩니다.
seller = payable(msg.sender): 컨트랙트를 배포하는 주체(보통 NFT의 소유자)의 주소를 seller 변수에 저장합니다. payable 키워드는 이 주소가 이더리움을 수신할 수 있도록 합니다.
highestBid = _startingBid: 경매의 시작 입찰 금액을 highestBid 변수에 설정합니다. 이 금액은 경매 동안 다른 입찰자들이 이 금액 이상을 제시해야 하는 최소 금액을 정의합니다.
constructor(address _nft, uint256 _nftId, uint256 _startingBid) {
nft = IERC721(_nft);
nftId = _nftId;
seller = payable(msg.sender);
highestBid = _startingBid;
}
- 접근 제한자:
external 키워드는 이 함수가 오직 외부에서만 호출될 수 있음을 나타냅니다. 즉, 다른 컨트랙트나 트랜잭션을 통해서만 이 함수에 접근할 수 있습니다.
조건 검사:
- require(!started, "started"): 이 조건은 started 변수가 false일 경우에만 함수가 실행되도록 합니다. 만약 이미 started가 true이면 "started"라는 에러 메시지와 함께 실행이 중단됩니다.
require(msg.sender == seller, "not seller"): 이 조건은 함수를 호출하는 주체(msg.sender)가 seller 변수에 저장된 주소와 일치할 때만 함수가 실행되도록 합니다. 만약 주소가 일치하지 않으면 "not seller"라는 에러 메시지와 함께 실행이 중단됩니다.
- NFT 이전:
nft.transferFrom(msg.sender, address(this), nftId): IERC721 인터페이스의 transferFrom 함수를 호출하여 msg.sender로부터 현재 컨트랙트(address(this))로 nftId에 해당하는 NFT를 이전합니다. 이는 경매 컨트랙트(English Auction Contract)가 NFT의 임시 소유권을 가지게 하여, 경매가 종료될 때 승리자에게 NFT를 안전하게 전송할 수 있도록 합니다.
- 경매 상태 업데이트:
started = true: 경매가 시작되었음을 나타내기 위해 started 변수를 true로 설정합니다.
endAt = block.timestamp + 7 days;: endAt 변수에 현재 블록 타임스탬프에 7일을 더한 값을 저장합니다(초 단위로 저장됩니다). 이는 경매 종료 시점을 설정합니다.
- 이벤트 발생:
emit Start(): Start 이벤트를 발생시켜 경매의 시작을 외부에 알립니다. 이 이벤트는 경매 참여자들에게 경매가 시작되었음을 알리는 신호로 작용합니다.
function start() external {
require(!started, "started");
require(msg.sender == seller, "not seller");
nft.transferFrom(msg.sender, address(this), nftId);
started = true;
endAt = block.timestamp + 7 days;
emit Start();
}
- 접근 제한자와 페이어블(payable):
external 키워드는 이 함수가 오직 외부에서만 호출될 수 있음을 나타냅니다.
payable 키워드는 이 함수가 이더리움을 전송받을 수 있도록 하며, 이는 입찰 시 전송되는 금액을 받기 위해 필요합니다.
- 조건 검사:
require(started, "not started");: 경매가 시작되었는지 확인합니다. 만약 started가 false이면 "not started"라는 메시지와 함께 실행을 중단합니다.
require(block.timestamp < endAt, "ended"): 현재 시간이 경매 종료 시간 endAt보다 이전인지 확인합니다. 만약 이미 경매가 종료되었다면 "ended"라는 메시지와 함께 실행을 중단합니다.
require(msg.value > highestBid, "value < highest"): 전송된 금액(msg.value)이 현재의 최고 입찰 금액(highestBid)보다 큰지 확인합니다. 만약 그렇지 않다면 "value < highest"라는 메시지와 함께 실행을 중단합니다.
- 최고 입찰자 갱신:
if (highestBidder != address(0)): 이전 최고 입찰자가 존재하는 경우 (즉, highestBidder 주소가 0 주소가 아닌 경우)
bids[highestBidder] += highestBid: 이전 최고 입찰자의 입찰금을 bids 매핑에 더해 입찰금을 갱신합니다. 이를 통해 이전 최고 입찰자가 다음에 입찰금을 철회할 수 있도록 합니다.
- 새로운 최고 입찰자 설정:
highestBidder = msg.sender: 현재 함수를 호출한 주소(msg.sender)를 새로운 최고 입찰자로 설정합니다.
highestBid = msg.value: 전송된 금액(msg.value)을 새로운 최고 입찰 금액으로 설정합니다.
- 이벤트 발생:
emit Bid(msg.sender, msg.value): Bid 이벤트를 발생시켜 새로운 입찰 정보를 외부에 알립니다. 이 이벤트는 입찰이 성공적으로 이루어졌음을 나타내며, 입찰자와 입찰 금액을 포함합니다.
function bid() external payable {
require(started, "not started");
require(block.timestamp < endAt, "ended");
require(msg.value > highestBid, "value < highest");
if (highestBidder != address(0)) {
bids[highestBidder] += highestBid;
}
highestBidder = msg.sender;
highestBid = msg.value;
emit Bid(msg.sender, msg.value);
}
- 접근 제한자:
external 키워드는 이 함수가 오직 외부에서만 호출될 수 있음을 나타냅니다. 즉, 다른 컨트랙트나 트랜잭션을 통해서만 이 함수에 접근할 수 있습니다.
- 입찰금 철회 로직:
uint256 bal = bids[msg.sender]: 함수를 호출한 주소(msg.sender)의 현재 입찰금을 bids 매핑에서 조회하여 bal 변수에 저장합니다.
bids[msg.sender] = 0: 해당 주소의 입찰금 정보를 bids 매핑에서 0으로 초기화하여, 다시 입찰금을 철회하려는 시도가 없도록 합니다.
- 금액 전송:
payable(msg.sender).transfer(bal): 초기화된 입찰금 bal을 함수를 호출한 주소(msg.sender)로 전송합니다. payable 키워드는 이더리움 주소가 이더를 수신할 수 있게 해주며, transfer 함수를 사용해 안전하게 이더를 해당 주소로 송금합니다.
- 이벤트 발생:
emit Withdraw(msg.sender, bal): Withdraw 이벤트를 발생시켜 입찰금 철회 정보를 외부에 알립니다. 이 이벤트는 입찰금 철회가 성공적으로 이루어졌음을 나타내며, 입찰금을 철회한 주소와 철회한 금액을 포함합니다.
function withdraw() external {
uint256 bal = bids[msg.sender];
bids[msg.sender] = 0;
payable(msg.sender).transfer(bal);
emit Withdraw(msg.sender, bal);
}
- 접근 제한자:
external 키워드는 이 함수가 오직 외부에서만 호출될 수 있음을 나타냅니다. 즉, 다른 컨트랙트나 트랜잭션을 통해서만 이 함수에 접근할 수 있습니다.
- 조건 검사:
require(started, "not started"): 경매가 시작되었는지 확인합니다. 만약 started가 false이면 "not started"라는 메시지와 함께 실행을 중단합니다.
require(block.timestamp >= endAt, "not ended"): 현재 시간이 경매 종료 시간 endAt 이후인지 확인합니다. 만약 아직 경매 시간이 남아 있다면 "not ended"라는 메시지와 함께 실행을 중단합니다.
require(!ended, "ended"): 경매가 이미 종료되었는지 확인합니다. 만약 ended가 true이면 "ended"라는 메시지와 함께 실행을 중단합니다.
- 경매 상태 업데이트:
ended = true: 경매의 종료 상태를 true로 설정하여, 경매가 종료되었음을 나타냅니다.
- NFT 및 금액 전송:
if (highestBidder != address(0)) : 최고 입찰자가 존재하는 경우
nft.safeTransferFrom(address(this), highestBidder, nftId): IERC721 인터페이스의 safeTransferFrom 함수를 사용하여 NFT를 현재 컨트랙트에서 최고 입찰자의 주소로 안전하게 전송합니다.
seller.transfer(highestBid): 최고 입찰 금액을 판매자의 주소로 전송합니다.
else : 최고 입찰자가 없는 경우
nft.safeTransferFrom(address(this), seller, nftId): NFT를 다시 판매자에게 전송합니다. safeTransferFrom 함수는 to 주소에서 토큰을 받았는지 확인하는 과정을 포함합니다.
- 이벤트 발생:
emit End(highestBidder, highestBid): End 이벤트를 발생시켜 경매의 종료를 외부에 알립니다. 이 이벤트는 최고 입찰자와 최고 입찰 금액을 포함하여, 경매의 결과를 공개합니다.
function end() external {
require(started, "not started");
require(block.timestamp >= endAt, "not ended");
require(!ended, "ended");
ended = true;
if (highestBidder != address(0)) {
nft.safeTransferFrom(address(this), highestBidder, nftId);
seller.transfer(highestBid);
} else {
nft.safeTransferFrom(address(this), seller, nftId);
}
emit End(highestBidder, highestBid);
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
interface IERC721 {
function safeTransferFrom(address from, address to, uint256 tokenId)
external;
function transferFrom(address, address, uint256) external;
}
contract EnglishAuction {
event Start();
event Bid(address indexed sender, uint256 amount);
event Withdraw(address indexed bidder, uint256 amount);
event End(address winner, uint256 amount);
IERC721 public nft;
uint256 public nftId;
address payable public seller;
uint256 public endAt;
bool public started;
bool public ended;
address public highestBidder;
uint256 public highestBid;
mapping(address => uint256) public bids;
constructor(address _nft, uint256 _nftId, uint256 _startingBid) {
nft = IERC721(_nft);
nftId = _nftId;
seller = payable(msg.sender);
highestBid = _startingBid;
}
function start() external {
require(!started, "started");
require(msg.sender == seller, "not seller");
nft.transferFrom(msg.sender, address(this), nftId);
started = true;
endAt = block.timestamp + 7 days;
emit Start();
}
function bid() external payable {
require(started, "not started");
require(block.timestamp < endAt, "ended");
require(msg.value > highestBid, "value < highest");
if (highestBidder != address(0)) {
bids[highestBidder] += highestBid;
}
highestBidder = msg.sender;
highestBid = msg.value;
emit Bid(msg.sender, msg.value);
}
function withdraw() external {
uint256 bal = bids[msg.sender];
bids[msg.sender] = 0;
payable(msg.sender).transfer(bal);
emit Withdraw(msg.sender, bal);
}
function end() external {
require(started, "not started");
require(block.timestamp >= endAt, "not ended");
require(!ended, "ended");
ended = true;
if (highestBidder != address(0)) {
nft.safeTransferFrom(address(this), highestBidder, nftId);
seller.transfer(highestBid);
} else {
nft.safeTransferFrom(address(this), seller, nftId);
}
emit End(highestBidder, highestBid);
}
}
Open in REMIX
토큰이 없을 경우 아래 ERC-721 smart contract를 사용하세요
// SPDX-License-Identifier: MIT
// Compatible with OpenZeppelin Contracts ^5.0.0
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract MyToken is ERC721, Ownable {
uint256 private _nextTokenId;
constructor()
ERC721("MyToken", "MTK")
Ownable(msg.sender)
{}
function safeMint(address to) public onlyOwner {
uint256 tokenId = _nextTokenId++;
_safeMint(to, tokenId);
}
}
이 컨트랙트는 mint 할 때마다 토큰아이디가 1씩 증각합니다.
MyToken contract를 컴파일 후 deploy합니다. 아래와 같이 세팅 후 deploy하면 됩니다.
NFT를 민트합니다. Deployed/Unpinned Contracts에서 deploy한 contract를 클릭합니다. 그 후 아래와 같은 버튼들 중 safeMint 버튼 옆에 주소 적는 빈칸에 본인이 원하는 주소(본인의 주소를 추천합니다)를 넣고 버튼을 클릭하면 됩니다.
- EnglishAuction contract를 deploy 합니다. 아래 이미지와 같이 만든 후 deploy 버튼을 누르면 됩니다. 이때 _NFT에는 위에서 deploy한 NFT 주소를, _NFTID에는 0(한개밖에 mint 안했기 때문에)을, _STARTINGBID에는 1ETH를 입력하기 위해서 1*10^18(wei 단위)을 넣어줍니다.
- EnglishAuction contract가 IERC721의 transferFrom함수를 사용하기 위해서는 해당 nft의 ID에 대한 approve가 되어 있어야 합니다. MyToken contract의 approve 버튼 옆에 EnglishAuction contract의 주소를 입력 후 클릭합니다.
3. 먼저 auction을 시작하기 위히서 start버튼을 누릅니다. 이제 옥션이 시작한겁니다. 이때 사인하는 주소가 English Auction contract를 deploy한 주소와 동일해야 합니다.
4. 파란색 버튼들을 실행해보면 모두 정상 작동하는거를 확인할 수 있습니다. 이때 아직 아무도 bid를 하지 않았기 때문에 이 주소는 비어 있습니다.
5. 다른 주소로 바꿉니다.
6. Value를 2ETH로 바꾼 후 bid 버튼을 누릅니다. contract의 balance가 2ETH로 바뀌고 highestBid와 highestBidder가 바뀐 것을 확인할 수 있습니다.
7. 주소와 bid하는 value를 바꿔가면 옥션을 진행합니다.
8. withdraw 함수는 해당 주소의 bid를 contract로부터 돌려받을 수 있습니다. 이때 highestBidder눈 withdraw 할 수 없습니다.
10. 7일 이후에 end 함수를 누르면 옥션을 정지하고 highestbid만큼의 ETH가 seller의 주소로 전달됩니다.