Skip to content

Commit

Permalink
Implement SBT signaling standard (#6)
Browse files Browse the repository at this point in the history
* Implement EIP-5192

* Add tests

* Update docs
  • Loading branch information
TomiOhl authored Nov 15, 2023
1 parent 03dc62f commit 92cc4bd
Show file tree
Hide file tree
Showing 9 changed files with 144 additions and 20 deletions.
2 changes: 2 additions & 0 deletions contracts/BasicGuildRewardNFT.sol
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ contract BasicGuildRewardNFT is

_safeMint(receiver, tokenId);

emit Locked(tokenId);

emit Claimed(receiver, tokenId);
}

Expand Down
4 changes: 0 additions & 4 deletions contracts/interfaces/IBasicGuildRewardNFT.sol
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,4 @@ interface IBasicGuildRewardNFT {

/// @notice Error thrown when the supplied signature is invalid.
error IncorrectSignature();

/// @notice Error thrown when trying to query info about a token that's not (yet) minted.
/// @param tokenId The queried id.
error NonExistentToken(uint256 tokenId);
}
20 changes: 20 additions & 0 deletions contracts/interfaces/IERC5192.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.0;

interface IERC5192 {
/// @notice Emitted when the locking status is changed to locked.
/// @dev If a token is minted and the status is locked, this event should be emitted.
/// @param tokenId The identifier for a token.
event Locked(uint256 tokenId);

/// @notice Emitted when the locking status is changed to unlocked.
/// @dev If a token is minted and the status is unlocked, this event should be emitted.
/// @param tokenId The identifier for a token.
event Unlocked(uint256 tokenId);

/// @notice Returns the locking status of an Soulbound Token
/// @dev SBTs assigned to zero address are considered invalid, and queries
/// about them do throw.
/// @param tokenId The identifier for an SBT.
function locked(uint256 tokenId) external view returns (bool);
}
17 changes: 15 additions & 2 deletions contracts/token/SoulboundERC721.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ pragma solidity 0.8.19;

/* solhint-disable max-line-length */

import { IERC5192 } from "../interfaces/IERC5192.sol";
import { ERC721Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol";
import { IERC721Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC721/IERC721Upgradeable.sol";
import { ERC721EnumerableUpgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721EnumerableUpgradeable.sol";
Expand All @@ -14,7 +15,11 @@ import { IERC721EnumerableUpgradeable } from "@openzeppelin/contracts-upgradeabl
/// @notice Allowance and transfer-related functions are disabled.
/// @dev Inheriting from upgradeable contracts here - even though we're using it in a non-upgradeable way,
/// we still want it to be initializable
contract SoulboundERC721 is ERC721Upgradeable, ERC721EnumerableUpgradeable {
contract SoulboundERC721 is ERC721Upgradeable, ERC721EnumerableUpgradeable, IERC5192 {
/// @notice Error thrown when trying to query info about a token that's not (yet) minted.
/// @param tokenId The queried id.
error NonExistentToken(uint256 tokenId);

/// @notice Error thrown when a function's execution is not possible, because this is a soulbound NFT.
error Soulbound();

Expand All @@ -28,7 +33,15 @@ contract SoulboundERC721 is ERC721Upgradeable, ERC721EnumerableUpgradeable {
function supportsInterface(
bytes4 interfaceId
) public view virtual override(ERC721EnumerableUpgradeable, ERC721Upgradeable) returns (bool) {
return interfaceId == type(IERC721EnumerableUpgradeable).interfaceId || super.supportsInterface(interfaceId);
return
interfaceId == type(IERC5192).interfaceId ||
interfaceId == type(IERC721EnumerableUpgradeable).interfaceId ||
super.supportsInterface(interfaceId);
}

function locked(uint256 tokenId) external view returns (bool) {
if (!_exists(tokenId)) revert NonExistentToken(tokenId);
return true;
}

function approve(
Expand Down
14 changes: 0 additions & 14 deletions docs/contracts/interfaces/IBasicGuildRewardNFT.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,17 +220,3 @@ error IncorrectSignature()

Error thrown when the supplied signature is invalid.

### NonExistentToken

```solidity
error NonExistentToken(uint256 tokenId)
```

Error thrown when trying to query info about a token that's not (yet) minted.

#### Parameters

| Name | Type | Description |
| ---- | ---- | ----------- |
| tokenId | uint256 | The queried id. |

60 changes: 60 additions & 0 deletions docs/contracts/interfaces/IERC5192.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# IERC5192

## Functions

### locked

```solidity
function locked(
uint256 tokenId
) external returns (bool)
```

Returns the locking status of an Soulbound Token

SBTs assigned to zero address are considered invalid, and queries
about them do throw.

#### Parameters

| Name | Type | Description |
| :--- | :--- | :---------- |
| `tokenId` | uint256 | The identifier for an SBT. |

## Events

### Locked

```solidity
event Locked(
uint256 tokenId
)
```

Emitted when the locking status is changed to locked.

If a token is minted and the status is locked, this event should be emitted.

#### Parameters

| Name | Type | Description |
| :--- | :--- | :---------- |
| `tokenId` | uint256 | The identifier for a token. |
### Unlocked

```solidity
event Unlocked(
uint256 tokenId
)
```

Emitted when the locking status is changed to unlocked.

If a token is minted and the status is unlocked, this event should be emitted.

#### Parameters

| Name | Type | Description |
| :--- | :--- | :---------- |
| `tokenId` | uint256 | The identifier for a token. |

33 changes: 33 additions & 0 deletions docs/contracts/token/SoulboundERC721.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,25 @@ See {IERC165-supportsInterface}.
| :--- | :--- | :---------- |
| `interfaceId` | bytes4 | |

### locked

```solidity
function locked(
uint256 tokenId
) external returns (bool)
```

Returns the locking status of an Soulbound Token

SBTs assigned to zero address are considered invalid, and queries
about them do throw.

#### Parameters

| Name | Type | Description |
| :--- | :--- | :---------- |
| `tokenId` | uint256 | The identifier for an SBT. |

### approve

```solidity
Expand Down Expand Up @@ -169,6 +188,20 @@ Still used for minting/burning.

## Custom errors

### NonExistentToken

```solidity
error NonExistentToken(uint256 tokenId)
```

Error thrown when trying to query info about a token that's not (yet) minted.

#### Parameters

| Name | Type | Description |
| ---- | ---- | ----------- |
| tokenId | uint256 | The queried id. |

### Soulbound

```solidity
Expand Down
7 changes: 7 additions & 0 deletions test/BasicGuildRewardNFT.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,13 @@ describe("BasicGuildRewardNFT", () => {
expect(await nft.ownerOf(tokenId)).to.eq(wallet0.address);
});

it("should emit Locked event when minting", async () => {
const tokenId = await nft.totalSupply();
await expect(nft.claim(wallet0.address, sampleUserId, sampleSignature, { value: fee + price }))
.to.emit(nft, "Locked")
.withArgs(tokenId);
});

it("should emit Claimed event", async () => {
await expect(
nft.claim(wallet0.address, sampleUserId, sampleSignature, {
Expand Down
7 changes: 7 additions & 0 deletions test/SoulboundERC721.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,11 @@ describe("SoulboundERC721", () => {
nft["safeTransferFrom(address,address,uint256,bytes)"](wallet0.address, randomWallet.address, 0, ethers.ZeroHash)
).to.be.revertedWithCustomError(BasicGuildRewardNFT, "Soulbound");
});

it("should have a locked function that throws for not minted tokens", async () => {
const tokenId = 1;
await expect(nft.locked(tokenId))
.to.be.revertedWithCustomError(BasicGuildRewardNFT, "NonExistentToken")
.withArgs(tokenId);
});
});

0 comments on commit 92cc4bd

Please sign in to comment.