[Ethereum] ERC-721A에 대해 알아보자
ERC-721A
개념
ERC-721A는 IERC721의 구현체로
여러 개의 NFT를 하나의 NFT와 비슷한 gas 비용으로 minting할 수 있게 해준다.
ERC-721 Enumerable과의 비교
ERC-721 Enumerable은 IERC721과 IERC721Enumberable를 구현하고 있는 추상 contract로
토큰 목록을 순회해서 조회하는 기능을 가지고 있다.
아래는 ERC-721 Enumerable과 ERC-721A의 성능을 비교한 자료이다.
대량의 토큰을 miniting하는 상황에서 ERC-721A가 적합한 것을 확인할 수 있다.

ERC-721A에서는 ERC-721 Enumerable과 비교했을 때 아래와 같은 부분을 최적화했다.
1. 토큰의 metadata를 중복으로 저장하는 부분을 제거했다.
2. ERC-721A에서는 batch mint를 지원하고 있는데 토큰의 소유자를 설정하는 로직에서
처음 한번만 설정하고 다음부터는 설정하지 않는다.

Azuki Contract
ERC-721A
_currentIndex는 토큰이 mint될 때마다 증가하는 값으로 총 발행량을 구할 때 사용된다.
_tokenApprovals은 토큰 아이디마다 approved된 주소를 저장한다.
_packedOwnerships와 _packedAddressData는 토큰 아이디마다 소유자와 정보를 저장하고 있는데
이름에서 알 수 있듯이 데이터를 저장하는 과정에서 구조체를 사용하지 않고 256 bit를 구획해서 사용한다.
uint256 private _currentIndex;
struct TokenApprovalRef {
address value;
}
mapping(uint256 => TokenApprovalRef) private _tokenApprovals;
mapping(uint256 => uint256) private _packedOwnerships;
mapping(address => uint256) private _packedAddressData;
아래는 토큰을 mint하는 로직으로 batch mint를 지원하는 것을 볼 수 있다.
quantity 만큼의 토큰을 mint하고 소유권을 지정하고 있다.
function _mint(address to, uint256 quantity) internal virtual {
uint256 startTokenId = _currentIndex;
if (quantity == 0) revert MintZeroQuantity();
_beforeTokenTransfers(address(0), to, startTokenId, quantity);
unchecked {
_packedAddressData[to] += quantity * ((1 << _BITPOS_NUMBER_MINTED) | 1);
_packedOwnerships[startTokenId] = _packOwnershipData(
to,
_nextInitializedFlag(quantity) | _nextExtraData(address(0), to, 0)
);
uint256 toMasked;
uint256 end = startTokenId + quantity;
assembly {
toMasked := and(to, _BITMASK_ADDRESS)
log4(
0,
0,
_TRANSFER_EVENT_SIGNATURE,
0,
toMasked,
startTokenId
)
for {
let tokenId := add(startTokenId, 1)
} iszero(eq(tokenId, end)) {
tokenId := add(tokenId, 1)
} {
log4(0, 0, _TRANSFER_EVENT_SIGNATURE, 0, toMasked, tokenId)
}
}
if (toMasked == 0) revert MintToZeroAddress();
_currentIndex = end;
}
_afterTokenTransfers(address(0), to, startTokenId, quantity);
}
아래는 mint 된 토큰의 총 발행량을 구하는 로직이다.
mint 함수에서 사용된 _currentIndex를 사용하는 것을 볼 수 있다.
function totalSupply() public view virtual override returns (uint256) {
unchecked {
return _currentIndex - _burnCounter - _startTokenId();
}
}
테스트
아래와 같은 contract를 만든다.
contract MyERC721A is ERC721A {
constructor() ERC721A("Myerc721A", "721A") {}
function _baseURI() internal view virtual override returns (string memory) {
return 'https://diydriller-bucket.s3.ap-northeast-2.amazonaws.com/nft/';
}
}
아래는 테스트 로직으로 다음을 검증한다.
1. name과 symbol이 일치하는지 검증한다.
2. user1에게 토큰 2개를 mint하고 총 발행량과 user1의 balance를 검증한다.
3. user1으로 user2에게 아이디가 1인 토큰을 approve한다.
Approval 이벤트가 발생했는지와 아이디가 1인 토큰의 approved된 주소가 user2인지를 검증한다.
4. user2로 user1으로부터 owner로 아이디가 1인 토큰을 전송한다.
Transfer 이벤트가 발생했는지와 아이디가 1인 토큰의 소유자가 owner인지를 검증한다.
const name = 'Myerc721A';
const symbol = '721A';
describe('ERC721A Test', async () => {
let contract: Contract;
let owner: SignerWithAddress;
let user1: SignerWithAddress;
let user2: SignerWithAddress;
it('Set Data for ERC721A Test', async () => {
[owner, user1, user2] = await ethers.getSigners();
});
describe('Test ERC721A Metadata', () => {
it('should get correct name, symbol for the ERC721A Contract', async () => {
const ERC721AFactory = await ethers.getContractFactory('MyERC721A');
contract = await ERC721AFactory.deploy();
await contract.deployed();
expect(await contract.name()).to.equal(name);
expect(await contract.symbol()).to.equal(symbol);
});
});
describe('Test Mint ERC721A', () => {
it('should mint corrrectly for the ERC721A Contract', async () => {
await contract.connect(user1).mint(2);
expect(await contract.totalSupply()).to.equal('2');
expect(await contract.balanceOf(user1.address)).to.equal('2');
});
});
describe('Test Approval ERC721A', () => {
it('should get approved for ERC721A Contract', async () => {
expect(await contract.connect(user1).approve(user2.address, '1'))
.to.emit(contract, 'Approval')
.withArgs(user1.address, user2.address, '1');
expect(await contract.getApproved('1')).to.equal(user2.address);
});
});
describe('Test TransferFrom ERC721A', () => {
it('should have ERC721A token after TransferFrom', async () => {
expect(await contract.connect(user2).transferFrom(user1.address, owner.address, '1'))
.to.emit(contract, 'Transfer')
.withArgs(user1.address, owner.address, '1');
expect(await contract.ownerOf(1)).to.equal(owner.address);
});
});
});