From 8fb9250ad74d406e45e2fb4f2ad5838c976a9e64 Mon Sep 17 00:00:00 2001 From: ra-phael <10075759+ra-phael@users.noreply.github.com> Date: Tue, 25 Jan 2022 16:54:49 +0000 Subject: [PATCH 1/9] add first option for staking --- .../ERC1238/extensions/ERC1238Stakable.sol | 61 +++++++ contracts/mocks/ERC1238StakableMock.sol | 55 ++++++ test/ERC1238/extensions/ERC1238Stakable.ts | 172 ++++++++++++++++++ 3 files changed, 288 insertions(+) create mode 100644 contracts/ERC1238/extensions/ERC1238Stakable.sol create mode 100644 contracts/mocks/ERC1238StakableMock.sol create mode 100644 test/ERC1238/extensions/ERC1238Stakable.ts diff --git a/contracts/ERC1238/extensions/ERC1238Stakable.sol b/contracts/ERC1238/extensions/ERC1238Stakable.sol new file mode 100644 index 0000000..0a94374 --- /dev/null +++ b/contracts/ERC1238/extensions/ERC1238Stakable.sol @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../ERC1238.sol"; + +/** + * @dev Proposal for ERC1238 tokens extension that make them 'stakable' + */ +abstract contract ERC1238Stakable is ERC1238 { + // Mapping owner => tokenId => stakeholder => stake size + mapping(address => mapping(uint256 => mapping(address => uint256))) private _stakes; + + function _beforeBurn( + address burner, + address from, + uint256 id, + uint256 amount + ) internal view virtual override { + require(burner == from || _stakes[from][id][burner] >= amount, "ERC1238Stakable: Unauthorized to burn tokens"); + } + + /** + * @dev Allows token owners to put their tokens at stake + * + * Calling this function again with the same stakeholder and id + * overrides the previous given allowance + * + * Requirements: + * + * + */ + function _increaseStake( + address stakeholder, + uint256 id, + uint256 amount + ) internal { + _stakes[msg.sender][id][stakeholder] += amount; + } + + function _decreaseStake( + address owner, + uint256 id, + uint256 amount + ) internal { + uint256 authorization = _stakes[owner][id][msg.sender]; + + require(authorization >= amount, "ERC1238Stakable: cannot decrease more than current stake"); + unchecked { + _stakes[owner][id][msg.sender] = authorization - amount; + } + } + + function stakeOf( + address owner, + uint256 id, + address stakeholder + ) public view returns (uint256) { + return _stakes[owner][id][stakeholder]; + } +} diff --git a/contracts/mocks/ERC1238StakableMock.sol b/contracts/mocks/ERC1238StakableMock.sol new file mode 100644 index 0000000..78aeda3 --- /dev/null +++ b/contracts/mocks/ERC1238StakableMock.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../ERC1238/ERC1238.sol"; +import "../ERC1238/extensions/ERC1238Stakable.sol"; + +/** + * @dev Mock contract for ERC1238 tokens using ERC1238Stakable extension rendering them 'stakable' + */ +contract ERC1238StakableMock is ERC1238, ERC1238Stakable { + constructor(string memory uri) ERC1238(uri) {} + + function _beforeBurn( + address burner, + address from, + uint256 id, + uint256 amount + ) internal view override(ERC1238, ERC1238Stakable) { + super._beforeBurn(burner, from, id, amount); + } + + function mint( + address to, + uint256 id, + uint256 amount, + bytes memory data + ) public { + _mint(to, id, amount, data); + } + + function burn( + address owner, + uint256 id, + uint256 amount + ) public { + _burn(owner, id, amount); + } + + function increaseStake( + address stakeholder, + uint256 id, + uint256 amount + ) external { + _increaseStake(stakeholder, id, amount); + } + + function decreaseStake( + address owner, + uint256 id, + uint256 amount + ) external { + _decreaseStake(owner, id, amount); + } +} diff --git a/test/ERC1238/extensions/ERC1238Stakable.ts b/test/ERC1238/extensions/ERC1238Stakable.ts new file mode 100644 index 0000000..aae6978 --- /dev/null +++ b/test/ERC1238/extensions/ERC1238Stakable.ts @@ -0,0 +1,172 @@ +import { artifacts, ethers, waffle } from "hardhat"; +import type { Artifact } from "hardhat/types"; +import type { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; +import { expect } from "chai"; + +import type { ERC1238StakableMock } from "../../../src/types/ERC1238StakableMock"; +import { toBN } from "../../utils/test-utils"; + +const BASE_URI = "https://token-cdn-domain/{id}.json"; + +describe("ERC1238URIStakable", function () { + let erc1238Stakable: ERC1238StakableMock; + let admin: SignerWithAddress; + let tokenHolder: SignerWithAddress; + let stakeholder: SignerWithAddress; + + before(async function () { + const signers: SignerWithAddress[] = await ethers.getSigners(); + admin = signers[0]; + tokenHolder = signers[1]; + stakeholder = signers[2]; + }); + + beforeEach(async function () { + const ERC1238StakableMockArtifact: Artifact = await artifacts.readArtifact("ERC1238StakableMock"); + erc1238Stakable = await waffle.deployContract(admin, ERC1238StakableMockArtifact, [BASE_URI]); + }); + + describe("Staking", () => { + const fungibleTokenId = 1; + const amountMintedFungible = toBN("1000"); + const nftID = 2; + const amountMintedNonFungible = 1; + + beforeEach(async () => { + // mint fungible tokens + await erc1238Stakable.connect(admin).mint(tokenHolder.address, fungibleTokenId, amountMintedFungible, []); + // mint an NFT + await erc1238Stakable.connect(admin).mint(tokenHolder.address, nftID, amountMintedNonFungible, []); + }); + + it("should let a token owner increase a stake", async () => { + await erc1238Stakable.connect(tokenHolder).increaseStake(stakeholder.address, nftID, 1); + + expect(await erc1238Stakable.stakeOf(tokenHolder.address, nftID, stakeholder.address)).to.eq(1); + }); + + it("should let a stakeholder burn a staked NFT", async () => { + // Given + expect(await erc1238Stakable.balanceOf(tokenHolder.address, nftID)).to.eq(amountMintedNonFungible); + + // When + await erc1238Stakable.connect(tokenHolder).increaseStake(stakeholder.address, nftID, 1); + + await erc1238Stakable.connect(stakeholder).burn(tokenHolder.address, nftID, 1); + + // Expect + expect(await erc1238Stakable.balanceOf(tokenHolder.address, nftID)).to.eq(0); + }); + + it("should let a stakeholder burn up to the amount of fungible tokens staked", async () => { + // Given + const stakedAmount = toBN("500"); + + expect(await erc1238Stakable.balanceOf(tokenHolder.address, fungibleTokenId)).to.eq(amountMintedFungible); + + // When + await erc1238Stakable.connect(tokenHolder).increaseStake(stakeholder.address, fungibleTokenId, stakedAmount); + + await erc1238Stakable.connect(stakeholder).burn(tokenHolder.address, fungibleTokenId, stakedAmount); + + // Expect + expect(await erc1238Stakable.balanceOf(tokenHolder.address, fungibleTokenId)).to.eq( + amountMintedFungible.sub(stakedAmount), + ); + }); + + it("should not let a stakeholder burn more that the staked amount", async () => { + const stakedAmount = toBN("500"); + const burnAmount = stakedAmount.add(toBN("1")); + + await erc1238Stakable.connect(tokenHolder).increaseStake(stakeholder.address, fungibleTokenId, stakedAmount); + + await expect( + erc1238Stakable.connect(stakeholder).burn(tokenHolder.address, fungibleTokenId, burnAmount), + ).to.be.revertedWith("ERC1238Stakable: Unauthorized to burn tokens"); + }); + + it("should let a token owner burn tokens before staking", async () => { + // Given + expect(await erc1238Stakable.balanceOf(tokenHolder.address, fungibleTokenId)).to.eq(amountMintedFungible); + + // When + await erc1238Stakable.connect(tokenHolder).burn(tokenHolder.address, fungibleTokenId, amountMintedFungible); + + // Expect + expect(await erc1238Stakable.balanceOf(tokenHolder.address, fungibleTokenId)).to.eq(0); + }); + + it("should let a token owner burn tokens after staking", async () => { + // Given + const stakedAmount = toBN("800"); + + expect(await erc1238Stakable.balanceOf(tokenHolder.address, fungibleTokenId)).to.eq(amountMintedFungible); + + // When + await erc1238Stakable.connect(tokenHolder).increaseStake(stakeholder.address, fungibleTokenId, stakedAmount); + + await erc1238Stakable.connect(tokenHolder).burn(tokenHolder.address, fungibleTokenId, stakedAmount); + + // Expect + expect(await erc1238Stakable.balanceOf(tokenHolder.address, fungibleTokenId)).to.eq( + amountMintedFungible.sub(stakedAmount), + ); + // "Burn" allowance is the same + expect(await erc1238Stakable.stakeOf(tokenHolder.address, fungibleTokenId, stakeholder.address)).to.eq( + stakedAmount, + ); + }); + + context("Decrease Stake", () => { + it("should let a stakeholder decrease a stake", async () => { + const stakedAmount = toBN("800"); + const amountToUnstake = toBN("300"); + + await erc1238Stakable.connect(tokenHolder).increaseStake(stakeholder.address, fungibleTokenId, stakedAmount); + + expect(await erc1238Stakable.stakeOf(tokenHolder.address, fungibleTokenId, stakeholder.address)).to.eq( + stakedAmount, + ); + + await erc1238Stakable.connect(stakeholder).decreaseStake(tokenHolder.address, fungibleTokenId, amountToUnstake); + + expect(await erc1238Stakable.stakeOf(tokenHolder.address, fungibleTokenId, stakeholder.address)).to.eq( + stakedAmount.sub(amountToUnstake), + ); + }); + + it("should let a stakeholder fully unstake", async () => { + const stakedAmount = toBN("800"); + const amountToUnstake = stakedAmount; + + await erc1238Stakable.connect(tokenHolder).increaseStake(stakeholder.address, fungibleTokenId, stakedAmount); + + expect(await erc1238Stakable.stakeOf(tokenHolder.address, fungibleTokenId, stakeholder.address)).to.eq( + stakedAmount, + ); + + await erc1238Stakable.connect(stakeholder).decreaseStake(tokenHolder.address, fungibleTokenId, amountToUnstake); + + expect(await erc1238Stakable.stakeOf(tokenHolder.address, fungibleTokenId, stakeholder.address)).to.eq(0); + }); + + it("should not let a token owner unstake", async () => { + const stakedAmount = toBN("600"); + await erc1238Stakable.connect(tokenHolder).increaseStake(stakeholder.address, fungibleTokenId, stakedAmount); + + expect(await erc1238Stakable.stakeOf(tokenHolder.address, fungibleTokenId, stakeholder.address)).to.eq( + stakedAmount, + ); + + await expect( + erc1238Stakable.connect(tokenHolder).decreaseStake(tokenHolder.address, fungibleTokenId, stakedAmount), + ).to.be.revertedWith("ERC1238Stakable: cannot decrease more than current stake"); + + expect(await erc1238Stakable.stakeOf(tokenHolder.address, fungibleTokenId, stakeholder.address)).to.eq( + stakedAmount, + ); + }); + }); + }); +}); From 88daf32f8cdb8d7ecb243222f6c0f96656d33c96 Mon Sep 17 00:00:00 2001 From: ra-phael <10075759+ra-phael@users.noreply.github.com> Date: Sat, 29 Jan 2022 11:18:41 +0000 Subject: [PATCH 2/9] rename tokenHolder to tokenOwner --- test/ERC1238/extensions/ERC1238Stakable.ts | 72 +++++++++++----------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/test/ERC1238/extensions/ERC1238Stakable.ts b/test/ERC1238/extensions/ERC1238Stakable.ts index aae6978..b93a8c6 100644 --- a/test/ERC1238/extensions/ERC1238Stakable.ts +++ b/test/ERC1238/extensions/ERC1238Stakable.ts @@ -11,13 +11,13 @@ const BASE_URI = "https://token-cdn-domain/{id}.json"; describe("ERC1238URIStakable", function () { let erc1238Stakable: ERC1238StakableMock; let admin: SignerWithAddress; - let tokenHolder: SignerWithAddress; + let tokenOwner: SignerWithAddress; let stakeholder: SignerWithAddress; before(async function () { const signers: SignerWithAddress[] = await ethers.getSigners(); admin = signers[0]; - tokenHolder = signers[1]; + tokenOwner = signers[1]; stakeholder = signers[2]; }); @@ -34,43 +34,43 @@ describe("ERC1238URIStakable", function () { beforeEach(async () => { // mint fungible tokens - await erc1238Stakable.connect(admin).mint(tokenHolder.address, fungibleTokenId, amountMintedFungible, []); + await erc1238Stakable.connect(admin).mint(tokenOwner.address, fungibleTokenId, amountMintedFungible, []); // mint an NFT - await erc1238Stakable.connect(admin).mint(tokenHolder.address, nftID, amountMintedNonFungible, []); + await erc1238Stakable.connect(admin).mint(tokenOwner.address, nftID, amountMintedNonFungible, []); }); it("should let a token owner increase a stake", async () => { - await erc1238Stakable.connect(tokenHolder).increaseStake(stakeholder.address, nftID, 1); + await erc1238Stakable.connect(tokenOwner).increaseStake(stakeholder.address, nftID, 1); - expect(await erc1238Stakable.stakeOf(tokenHolder.address, nftID, stakeholder.address)).to.eq(1); + expect(await erc1238Stakable.stakeOf(tokenOwner.address, nftID, stakeholder.address)).to.eq(1); }); it("should let a stakeholder burn a staked NFT", async () => { // Given - expect(await erc1238Stakable.balanceOf(tokenHolder.address, nftID)).to.eq(amountMintedNonFungible); + expect(await erc1238Stakable.balanceOf(tokenOwner.address, nftID)).to.eq(amountMintedNonFungible); // When - await erc1238Stakable.connect(tokenHolder).increaseStake(stakeholder.address, nftID, 1); + await erc1238Stakable.connect(tokenOwner).increaseStake(stakeholder.address, nftID, 1); - await erc1238Stakable.connect(stakeholder).burn(tokenHolder.address, nftID, 1); + await erc1238Stakable.connect(stakeholder).burn(tokenOwner.address, nftID, 1); // Expect - expect(await erc1238Stakable.balanceOf(tokenHolder.address, nftID)).to.eq(0); + expect(await erc1238Stakable.balanceOf(tokenOwner.address, nftID)).to.eq(0); }); it("should let a stakeholder burn up to the amount of fungible tokens staked", async () => { // Given const stakedAmount = toBN("500"); - expect(await erc1238Stakable.balanceOf(tokenHolder.address, fungibleTokenId)).to.eq(amountMintedFungible); + expect(await erc1238Stakable.balanceOf(tokenOwner.address, fungibleTokenId)).to.eq(amountMintedFungible); // When - await erc1238Stakable.connect(tokenHolder).increaseStake(stakeholder.address, fungibleTokenId, stakedAmount); + await erc1238Stakable.connect(tokenOwner).increaseStake(stakeholder.address, fungibleTokenId, stakedAmount); - await erc1238Stakable.connect(stakeholder).burn(tokenHolder.address, fungibleTokenId, stakedAmount); + await erc1238Stakable.connect(stakeholder).burn(tokenOwner.address, fungibleTokenId, stakedAmount); // Expect - expect(await erc1238Stakable.balanceOf(tokenHolder.address, fungibleTokenId)).to.eq( + expect(await erc1238Stakable.balanceOf(tokenOwner.address, fungibleTokenId)).to.eq( amountMintedFungible.sub(stakedAmount), ); }); @@ -79,41 +79,41 @@ describe("ERC1238URIStakable", function () { const stakedAmount = toBN("500"); const burnAmount = stakedAmount.add(toBN("1")); - await erc1238Stakable.connect(tokenHolder).increaseStake(stakeholder.address, fungibleTokenId, stakedAmount); + await erc1238Stakable.connect(tokenOwner).increaseStake(stakeholder.address, fungibleTokenId, stakedAmount); await expect( - erc1238Stakable.connect(stakeholder).burn(tokenHolder.address, fungibleTokenId, burnAmount), + erc1238Stakable.connect(stakeholder).burn(tokenOwner.address, fungibleTokenId, burnAmount), ).to.be.revertedWith("ERC1238Stakable: Unauthorized to burn tokens"); }); it("should let a token owner burn tokens before staking", async () => { // Given - expect(await erc1238Stakable.balanceOf(tokenHolder.address, fungibleTokenId)).to.eq(amountMintedFungible); + expect(await erc1238Stakable.balanceOf(tokenOwner.address, fungibleTokenId)).to.eq(amountMintedFungible); // When - await erc1238Stakable.connect(tokenHolder).burn(tokenHolder.address, fungibleTokenId, amountMintedFungible); + await erc1238Stakable.connect(tokenOwner).burn(tokenOwner.address, fungibleTokenId, amountMintedFungible); // Expect - expect(await erc1238Stakable.balanceOf(tokenHolder.address, fungibleTokenId)).to.eq(0); + expect(await erc1238Stakable.balanceOf(tokenOwner.address, fungibleTokenId)).to.eq(0); }); it("should let a token owner burn tokens after staking", async () => { // Given const stakedAmount = toBN("800"); - expect(await erc1238Stakable.balanceOf(tokenHolder.address, fungibleTokenId)).to.eq(amountMintedFungible); + expect(await erc1238Stakable.balanceOf(tokenOwner.address, fungibleTokenId)).to.eq(amountMintedFungible); // When - await erc1238Stakable.connect(tokenHolder).increaseStake(stakeholder.address, fungibleTokenId, stakedAmount); + await erc1238Stakable.connect(tokenOwner).increaseStake(stakeholder.address, fungibleTokenId, stakedAmount); - await erc1238Stakable.connect(tokenHolder).burn(tokenHolder.address, fungibleTokenId, stakedAmount); + await erc1238Stakable.connect(tokenOwner).burn(tokenOwner.address, fungibleTokenId, stakedAmount); // Expect - expect(await erc1238Stakable.balanceOf(tokenHolder.address, fungibleTokenId)).to.eq( + expect(await erc1238Stakable.balanceOf(tokenOwner.address, fungibleTokenId)).to.eq( amountMintedFungible.sub(stakedAmount), ); // "Burn" allowance is the same - expect(await erc1238Stakable.stakeOf(tokenHolder.address, fungibleTokenId, stakeholder.address)).to.eq( + expect(await erc1238Stakable.stakeOf(tokenOwner.address, fungibleTokenId, stakeholder.address)).to.eq( stakedAmount, ); }); @@ -123,15 +123,15 @@ describe("ERC1238URIStakable", function () { const stakedAmount = toBN("800"); const amountToUnstake = toBN("300"); - await erc1238Stakable.connect(tokenHolder).increaseStake(stakeholder.address, fungibleTokenId, stakedAmount); + await erc1238Stakable.connect(tokenOwner).increaseStake(stakeholder.address, fungibleTokenId, stakedAmount); - expect(await erc1238Stakable.stakeOf(tokenHolder.address, fungibleTokenId, stakeholder.address)).to.eq( + expect(await erc1238Stakable.stakeOf(tokenOwner.address, fungibleTokenId, stakeholder.address)).to.eq( stakedAmount, ); - await erc1238Stakable.connect(stakeholder).decreaseStake(tokenHolder.address, fungibleTokenId, amountToUnstake); + await erc1238Stakable.connect(stakeholder).decreaseStake(tokenOwner.address, fungibleTokenId, amountToUnstake); - expect(await erc1238Stakable.stakeOf(tokenHolder.address, fungibleTokenId, stakeholder.address)).to.eq( + expect(await erc1238Stakable.stakeOf(tokenOwner.address, fungibleTokenId, stakeholder.address)).to.eq( stakedAmount.sub(amountToUnstake), ); }); @@ -140,30 +140,30 @@ describe("ERC1238URIStakable", function () { const stakedAmount = toBN("800"); const amountToUnstake = stakedAmount; - await erc1238Stakable.connect(tokenHolder).increaseStake(stakeholder.address, fungibleTokenId, stakedAmount); + await erc1238Stakable.connect(tokenOwner).increaseStake(stakeholder.address, fungibleTokenId, stakedAmount); - expect(await erc1238Stakable.stakeOf(tokenHolder.address, fungibleTokenId, stakeholder.address)).to.eq( + expect(await erc1238Stakable.stakeOf(tokenOwner.address, fungibleTokenId, stakeholder.address)).to.eq( stakedAmount, ); - await erc1238Stakable.connect(stakeholder).decreaseStake(tokenHolder.address, fungibleTokenId, amountToUnstake); + await erc1238Stakable.connect(stakeholder).decreaseStake(tokenOwner.address, fungibleTokenId, amountToUnstake); - expect(await erc1238Stakable.stakeOf(tokenHolder.address, fungibleTokenId, stakeholder.address)).to.eq(0); + expect(await erc1238Stakable.stakeOf(tokenOwner.address, fungibleTokenId, stakeholder.address)).to.eq(0); }); it("should not let a token owner unstake", async () => { const stakedAmount = toBN("600"); - await erc1238Stakable.connect(tokenHolder).increaseStake(stakeholder.address, fungibleTokenId, stakedAmount); + await erc1238Stakable.connect(tokenOwner).increaseStake(stakeholder.address, fungibleTokenId, stakedAmount); - expect(await erc1238Stakable.stakeOf(tokenHolder.address, fungibleTokenId, stakeholder.address)).to.eq( + expect(await erc1238Stakable.stakeOf(tokenOwner.address, fungibleTokenId, stakeholder.address)).to.eq( stakedAmount, ); await expect( - erc1238Stakable.connect(tokenHolder).decreaseStake(tokenHolder.address, fungibleTokenId, stakedAmount), + erc1238Stakable.connect(tokenOwner).decreaseStake(tokenOwner.address, fungibleTokenId, stakedAmount), ).to.be.revertedWith("ERC1238Stakable: cannot decrease more than current stake"); - expect(await erc1238Stakable.stakeOf(tokenHolder.address, fungibleTokenId, stakeholder.address)).to.eq( + expect(await erc1238Stakable.stakeOf(tokenOwner.address, fungibleTokenId, stakeholder.address)).to.eq( stakedAmount, ); }); From a506aa362a5282733f3f0eeb5f482da93fc0a558 Mon Sep 17 00:00:00 2001 From: ra-phael <10075759+ra-phael@users.noreply.github.com> Date: Sat, 29 Jan 2022 15:07:20 +0000 Subject: [PATCH 3/9] introduce second option for staking --- .../ERC1238/extensions/ERC1238Holdable.sol | 60 ++++++ .../ERC1238/extensions/IERC1238Holdable.sol | 26 +++ contracts/mocks/ERC1238HoldableMock.sol | 74 +++++++ test/ERC1238/extensions/ERC1238Holdable.ts | 190 ++++++++++++++++++ 4 files changed, 350 insertions(+) create mode 100644 contracts/ERC1238/extensions/ERC1238Holdable.sol create mode 100644 contracts/ERC1238/extensions/IERC1238Holdable.sol create mode 100644 contracts/mocks/ERC1238HoldableMock.sol create mode 100644 test/ERC1238/extensions/ERC1238Holdable.ts diff --git a/contracts/ERC1238/extensions/ERC1238Holdable.sol b/contracts/ERC1238/extensions/ERC1238Holdable.sol new file mode 100644 index 0000000..f96d4aa --- /dev/null +++ b/contracts/ERC1238/extensions/ERC1238Holdable.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../ERC1238.sol"; +import "./IERC1238Holdable.sol"; + +/** + * @dev Proposal for ERC1238 tokens extension that allow addresses + * to hold tokens on behalf of others (escrow) + */ +abstract contract ERC1238Holdable is IERC1238Holdable, ERC1238 { + // Mapping holder => id => balance + mapping(address => mapping(uint256 => uint256)) private _escrowedBalances; + + function escrowedBalance(address holder, uint256 id) public view override returns (uint256) { + return _escrowedBalances[holder][id]; + } + + function _beforeMint( + address minter, + address to, + uint256 id, + uint256 amount, + bytes memory data + ) internal virtual override { + // set the token recipient as first holder by default when tokens are minted + _escrowedBalances[to][id] += amount; + } + + function _beforeBurn( + address burner, + address from, + uint256 id, + uint256 amount + ) internal virtual override { + require(burner == from, "ERC1238Holdable: Unauthorized to burn tokens"); + require(_escrowedBalances[burner][id] >= amount, "ERC1238Holdable: Amount to burn exceeds amount held"); + + _escrowedBalances[burner][id] -= amount; + } + + function _entrust( + address to, + uint256 id, + uint256 amount + ) internal virtual { + require(to != address(0), "ERC1238Holdable: transfer to the zero address"); + + address from = msg.sender; + + uint256 fromBalance = _escrowedBalances[from][id]; + require(fromBalance >= amount, "ERC1238Holdable: amount exceeds balance held"); + + _escrowedBalances[from][id] -= amount; + _escrowedBalances[to][id] += amount; + + emit Entrust(from, to, id, amount); + } +} diff --git a/contracts/ERC1238/extensions/IERC1238Holdable.sol b/contracts/ERC1238/extensions/IERC1238Holdable.sol new file mode 100644 index 0000000..172bfad --- /dev/null +++ b/contracts/ERC1238/extensions/IERC1238Holdable.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../IERC1238.sol"; + +/** + * @dev Proposal of an interface for ERC1238 token with storage based token URI management. + */ +interface IERC1238Holdable is IERC1238 { + /** + * @dev Event emitted when `from` entrusts `to` with `amount` of tokens with token `id` + */ + event Entrust(address from, address to, uint256 indexed id, uint256 amount); + + /** + * @dev Returns the balance of a token holder for a given `id` + */ + function escrowedBalance(address holder, uint256 id) external view returns (uint256); + + function entrust( + address to, + uint256 id, + uint256 amount + ) external; +} diff --git a/contracts/mocks/ERC1238HoldableMock.sol b/contracts/mocks/ERC1238HoldableMock.sol new file mode 100644 index 0000000..c167ba2 --- /dev/null +++ b/contracts/mocks/ERC1238HoldableMock.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../ERC1238/ERC1238.sol"; +import "../ERC1238/extensions/ERC1238Holdable.sol"; + +/** + * @dev Mock contract for ERC1238 tokens using ERC1238Holdable extension + */ +contract ERC1238HoldableMock is ERC1238, ERC1238Holdable { + constructor(string memory uri) ERC1238(uri) {} + + function _beforeMint( + address minter, + address to, + uint256 id, + uint256 amount, + bytes memory data + ) internal override(ERC1238, ERC1238Holdable) { + super._beforeMint(minter, to, id, amount, data); + } + + function _beforeBurn( + address burner, + address from, + uint256 id, + uint256 amount + ) internal override(ERC1238, ERC1238Holdable) { + super._beforeBurn(burner, from, id, amount); + } + + function mint( + address to, + uint256 id, + uint256 amount, + bytes memory data + ) public { + _mint(to, id, amount, data); + } + + function mintBatch( + address to, + uint256[] memory ids, + uint256[] memory amounts, + bytes memory data + ) public { + _mintBatch(to, ids, amounts, data); + } + + function burn( + address owner, + uint256 id, + uint256 amount + ) public { + _burn(owner, id, amount); + } + + function burnBatch( + address owner, + uint256[] memory ids, + uint256[] memory amounts + ) public { + _burnBatch(owner, ids, amounts); + } + + function entrust( + address to, + uint256 id, + uint256 amount + ) public override { + _entrust(to, id, amount); + } +} diff --git a/test/ERC1238/extensions/ERC1238Holdable.ts b/test/ERC1238/extensions/ERC1238Holdable.ts new file mode 100644 index 0000000..2b46a1a --- /dev/null +++ b/test/ERC1238/extensions/ERC1238Holdable.ts @@ -0,0 +1,190 @@ +import { artifacts, ethers, waffle } from "hardhat"; +import type { Artifact } from "hardhat/types"; +import type { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; +import { expect } from "chai"; + +import type { ERC1238HoldableMock } from "../../../src/types/ERC1238HoldableMock"; +import { ZERO_ADDRESS } from "../../utils/test-utils"; + +const BASE_URI = "https://token-cdn-domain/{id}.json"; + +describe("ERC1238URIHoldable", function () { + let erc1238Holdable: ERC1238HoldableMock; + let admin: SignerWithAddress; + let tokenOwner: SignerWithAddress; + let tokenHolder1: SignerWithAddress; + let tokenHolder2: SignerWithAddress; + + const tokenId = 888888; + const mintAmount = 98765432; + const data = "0x12345678"; + + before(async function () { + const signers: SignerWithAddress[] = await ethers.getSigners(); + admin = signers[0]; + tokenOwner = signers[1]; + tokenHolder1 = signers[2]; + tokenHolder2 = signers[3]; + }); + + beforeEach(async function () { + const ERC1238HoldableMockArtifact: Artifact = await artifacts.readArtifact("ERC1238HoldableMock"); + erc1238Holdable = await waffle.deployContract(admin, ERC1238HoldableMockArtifact, [BASE_URI]); + }); + + describe("Minting", () => { + it("should set the the token recipient as first holder", async () => { + await erc1238Holdable.mint(tokenOwner.address, tokenId, mintAmount, data); + + expect(await erc1238Holdable.escrowedBalance(tokenOwner.address, tokenId)).to.eq(mintAmount); + }); + + it("should update the held balance when minting multiple times", async () => { + const firstAmount = 1000; + const secondAmount = 200; + await erc1238Holdable.mint(tokenOwner.address, tokenId, firstAmount, data); + + expect(await erc1238Holdable.escrowedBalance(tokenOwner.address, tokenId)).to.eq(firstAmount); + + await erc1238Holdable.mint(tokenOwner.address, tokenId, secondAmount, data); + + expect(await erc1238Holdable.escrowedBalance(tokenOwner.address, tokenId)).to.eq(firstAmount + secondAmount); + }); + }); + + describe("Escrow", () => { + it("should not allow an escrow to the zero address", async () => { + await erc1238Holdable.mint(tokenOwner.address, tokenId, mintAmount, data); + + const tx = erc1238Holdable.connect(tokenOwner).entrust(ZERO_ADDRESS, tokenId, mintAmount); + + await expect(tx).to.be.revertedWith("ERC1238Holdable: transfer to the zero address"); + }); + + context("Full Escrow", () => { + it("should let a token owner put all their token in escrow", async () => { + await erc1238Holdable.mint(tokenOwner.address, tokenId, mintAmount, data); + + // tokenOwner entrusts tokenHolder1 with their tokens + await erc1238Holdable.connect(tokenOwner).entrust(tokenHolder1.address, tokenId, mintAmount); + + // tokenOwner does not hold the tokens anymore + expect(await erc1238Holdable.escrowedBalance(tokenOwner.address, tokenId)).to.eq(0); + // tokenHolder1 does hold them + expect(await erc1238Holdable.escrowedBalance(tokenHolder1.address, tokenId)).to.eq(mintAmount); + // tokenOwner is still the owner of these tokens + expect(await erc1238Holdable.balanceOf(tokenOwner.address, tokenId)).to.eq(mintAmount); + }); + + it("should let a holder transfer tokens to another holder", async () => { + await erc1238Holdable.mint(tokenOwner.address, tokenId, mintAmount, data); + + // tokenOwner entrusts tokenHolder1 with their tokens + await erc1238Holdable.connect(tokenOwner).entrust(tokenHolder1.address, tokenId, mintAmount); + // tokenHolder1 entrusts tokenHolder2 with these same tokens belonging to tokenOwner + await erc1238Holdable.connect(tokenHolder1).entrust(tokenHolder2.address, tokenId, mintAmount); + + // tokenOwner does not hold the tokens anymore + expect(await erc1238Holdable.escrowedBalance(tokenOwner.address, tokenId)).to.eq(0); + // tokenHolder1 does not hold them + expect(await erc1238Holdable.escrowedBalance(tokenHolder1.address, tokenId)).to.eq(0); + // tokenHolder2 does hold them + expect(await erc1238Holdable.escrowedBalance(tokenHolder2.address, tokenId)).to.eq(mintAmount); + // tokenOwner is still the owner of these tokens + expect(await erc1238Holdable.balanceOf(tokenOwner.address, tokenId)).to.eq(mintAmount); + }); + + it("should let a holder transfer tokens back to their owner", async () => { + await erc1238Holdable.mint(tokenOwner.address, tokenId, mintAmount, data); + + // tokenOwner entrusts tokenHolder1 with their tokens + await erc1238Holdable.connect(tokenOwner).entrust(tokenHolder1.address, tokenId, mintAmount); + // tokenHolder1 transfers them back to tokenOwner + await erc1238Holdable.connect(tokenHolder1).entrust(tokenOwner.address, tokenId, mintAmount); + + // tokenHolder1 does not hold the tokens anymore + expect(await erc1238Holdable.escrowedBalance(tokenHolder1.address, tokenId)).to.eq(0); + // tokenOwner does hold them + expect(await erc1238Holdable.escrowedBalance(tokenOwner.address, tokenId)).to.eq(mintAmount); + // tokenOwner is still the owner of these tokens + expect(await erc1238Holdable.balanceOf(tokenOwner.address, tokenId)).to.eq(mintAmount); + }); + }); + + context("Partial Escrow", () => { + const escrowedAmount = mintAmount - 1000; + it("should let a token owner put all their token in escrow", async () => { + await erc1238Holdable.mint(tokenOwner.address, tokenId, mintAmount, data); + + // tokenOwner entrusts tokenHolder1 with some of their tokens + await erc1238Holdable.connect(tokenOwner).entrust(tokenHolder1.address, tokenId, escrowedAmount); + + // tokenOwner holds the remaining amount of tokens + expect(await erc1238Holdable.escrowedBalance(tokenOwner.address, tokenId)).to.eq(mintAmount - escrowedAmount); + // tokenHolder1 holds the escrowed amount + expect(await erc1238Holdable.escrowedBalance(tokenHolder1.address, tokenId)).to.eq(escrowedAmount); + // tokenOwner is still the owner of all the tokens + expect(await erc1238Holdable.balanceOf(tokenOwner.address, tokenId)).to.eq(mintAmount); + }); + + it("should let a token holder transfer the escrowed amount", async () => { + await erc1238Holdable.mint(tokenOwner.address, tokenId, mintAmount, data); + + // tokenOwner entrusts tokenHolder1 with some their tokens + await erc1238Holdable.connect(tokenOwner).entrust(tokenHolder1.address, tokenId, escrowedAmount); + // tokenHolder1 entrusts tokenHolder2 with these tokens + await erc1238Holdable.connect(tokenHolder1).entrust(tokenHolder2.address, tokenId, escrowedAmount); + + // tokenOwner holds the remaining amount of tokens + expect(await erc1238Holdable.escrowedBalance(tokenOwner.address, tokenId)).to.eq(mintAmount - escrowedAmount); + // tokenHolder1 does not hold any tokens + expect(await erc1238Holdable.escrowedBalance(tokenHolder1.address, tokenId)).to.eq(0); + // tokenHolder2 does hold some of them + expect(await erc1238Holdable.escrowedBalance(tokenHolder2.address, tokenId)).to.eq(escrowedAmount); + // tokenOwner is still the owner of these tokens + expect(await erc1238Holdable.balanceOf(tokenOwner.address, tokenId)).to.eq(mintAmount); + }); + }); + }); + + describe("Burning", () => { + beforeEach(async () => { + await erc1238Holdable.mint(tokenOwner.address, tokenId, mintAmount, data); + }); + + it("should let a token owner burn all of their tokens", async () => { + await erc1238Holdable.connect(tokenOwner).burn(tokenOwner.address, tokenId, mintAmount); + + expect(await erc1238Holdable.escrowedBalance(tokenOwner.address, tokenId)).to.eq(0); + expect(await erc1238Holdable.balanceOf(tokenOwner.address, tokenId)).to.eq(0); + }); + + it("should let a token owner burn some of their tokens", async () => { + const amountToBurn = mintAmount - 1000; + await erc1238Holdable.connect(tokenOwner).burn(tokenOwner.address, tokenId, amountToBurn); + + expect(await erc1238Holdable.escrowedBalance(tokenOwner.address, tokenId)).to.eq(mintAmount - amountToBurn); + expect(await erc1238Holdable.balanceOf(tokenOwner.address, tokenId)).to.eq(mintAmount - amountToBurn); + }); + + it("should not give a token holder the right to burn tokens", async () => { + await erc1238Holdable.connect(tokenOwner).entrust(tokenHolder1.address, tokenId, mintAmount); + + await expect( + erc1238Holdable.connect(tokenHolder1).burn(tokenOwner.address, tokenId, mintAmount), + ).to.be.revertedWith("ERC1238Holdable: Unauthorized to burn tokens"); + }); + + it("should not let a token owner burn tokens they do not hold", async () => { + const escrowedAmount = 2000; + + await erc1238Holdable.connect(tokenOwner).entrust(tokenHolder1.address, tokenId, escrowedAmount); + + const amountHeldByOwner = mintAmount - escrowedAmount; + + await expect( + erc1238Holdable.connect(tokenOwner).burn(tokenOwner.address, tokenId, amountHeldByOwner + 1), + ).to.be.revertedWith("ERC1238Holdable: Amount to burn exceeds amount held"); + }); + }); +}); From 68f4ff028d058e99ad2aa70483e7e11eaa9b06a4 Mon Sep 17 00:00:00 2001 From: ra-phael <10075759+ra-phael@users.noreply.github.com> Date: Sat, 29 Jan 2022 15:07:58 +0000 Subject: [PATCH 4/9] small change --- contracts/ERC1238/extensions/ERC1238Stakable.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/ERC1238/extensions/ERC1238Stakable.sol b/contracts/ERC1238/extensions/ERC1238Stakable.sol index 0a94374..0f011db 100644 --- a/contracts/ERC1238/extensions/ERC1238Stakable.sol +++ b/contracts/ERC1238/extensions/ERC1238Stakable.sol @@ -8,7 +8,7 @@ import "../ERC1238.sol"; * @dev Proposal for ERC1238 tokens extension that make them 'stakable' */ abstract contract ERC1238Stakable is ERC1238 { - // Mapping owner => tokenId => stakeholder => stake size + // Mapping owner => token id => stakeholder => stake size mapping(address => mapping(uint256 => mapping(address => uint256))) private _stakes; function _beforeBurn( From 54cf1eae008b165ca099a5655bfd3fb36970645b Mon Sep 17 00:00:00 2001 From: ra-phael <10075759+ra-phael@users.noreply.github.com> Date: Wed, 2 Feb 2022 20:07:15 +0000 Subject: [PATCH 5/9] fix typos --- test/ERC1238/extensions/ERC1238Holdable.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/test/ERC1238/extensions/ERC1238Holdable.ts b/test/ERC1238/extensions/ERC1238Holdable.ts index 2b46a1a..71d30bd 100644 --- a/test/ERC1238/extensions/ERC1238Holdable.ts +++ b/test/ERC1238/extensions/ERC1238Holdable.ts @@ -1,8 +1,7 @@ -import { artifacts, ethers, waffle } from "hardhat"; -import type { Artifact } from "hardhat/types"; import type { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; import { expect } from "chai"; - +import { artifacts, ethers, waffle } from "hardhat"; +import type { Artifact } from "hardhat/types"; import type { ERC1238HoldableMock } from "../../../src/types/ERC1238HoldableMock"; import { ZERO_ADDRESS } from "../../utils/test-utils"; @@ -33,13 +32,13 @@ describe("ERC1238URIHoldable", function () { }); describe("Minting", () => { - it("should set the the token recipient as first holder", async () => { + it("should set the token recipient as first holder", async () => { await erc1238Holdable.mint(tokenOwner.address, tokenId, mintAmount, data); expect(await erc1238Holdable.escrowedBalance(tokenOwner.address, tokenId)).to.eq(mintAmount); }); - it("should update the held balance when minting multiple times", async () => { + it("should update the balance held when minting multiple times", async () => { const firstAmount = 1000; const secondAmount = 200; await erc1238Holdable.mint(tokenOwner.address, tokenId, firstAmount, data); @@ -113,7 +112,7 @@ describe("ERC1238URIHoldable", function () { context("Partial Escrow", () => { const escrowedAmount = mintAmount - 1000; - it("should let a token owner put all their token in escrow", async () => { + it("should let a token owner put some of their token in escrow", async () => { await erc1238Holdable.mint(tokenOwner.address, tokenId, mintAmount, data); // tokenOwner entrusts tokenHolder1 with some of their tokens From 4c4c9b5a40557f25446fa603c5cf52fa4d39c790 Mon Sep 17 00:00:00 2001 From: ra-phael <10075759+ra-phael@users.noreply.github.com> Date: Wed, 2 Feb 2022 21:55:42 +0000 Subject: [PATCH 6/9] add comment --- contracts/ERC1238/extensions/ERC1238Holdable.sol | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/contracts/ERC1238/extensions/ERC1238Holdable.sol b/contracts/ERC1238/extensions/ERC1238Holdable.sol index f96d4aa..ea6cdb1 100644 --- a/contracts/ERC1238/extensions/ERC1238Holdable.sol +++ b/contracts/ERC1238/extensions/ERC1238Holdable.sol @@ -40,6 +40,11 @@ abstract contract ERC1238Holdable is IERC1238Holdable, ERC1238 { _escrowedBalances[burner][id] -= amount; } + /** + * @dev Lets sender entrusts `to` with `amount` + * of tokens which gets transferred between their respective escrowedBalances + * + */ function _entrust( address to, uint256 id, From 2b557dee99622a12a46c67c3be258c77740803f0 Mon Sep 17 00:00:00 2001 From: ra-phael <10075759+ra-phael@users.noreply.github.com> Date: Wed, 2 Feb 2022 21:56:32 +0000 Subject: [PATCH 7/9] decrease stakeholder stake when burning --- .../ERC1238/extensions/ERC1238Stakable.sol | 56 +++++++++++++------ contracts/mocks/ERC1238StakableMock.sol | 2 +- 2 files changed, 41 insertions(+), 17 deletions(-) diff --git a/contracts/ERC1238/extensions/ERC1238Stakable.sol b/contracts/ERC1238/extensions/ERC1238Stakable.sol index 0f011db..cba3265 100644 --- a/contracts/ERC1238/extensions/ERC1238Stakable.sol +++ b/contracts/ERC1238/extensions/ERC1238Stakable.sol @@ -11,23 +11,38 @@ abstract contract ERC1238Stakable is ERC1238 { // Mapping owner => token id => stakeholder => stake size mapping(address => mapping(uint256 => mapping(address => uint256))) private _stakes; + function stakeOf( + address owner, + uint256 id, + address stakeholder + ) public view returns (uint256) { + return _stakes[owner][id][stakeholder]; + } + + /** + * @dev Called before tokens are burned + * + * Requirements: + * - `burner` and `from` are the same account OR + * - `from` entrusted `burner` with at least `amount` of tokens with id `id` + */ function _beforeBurn( address burner, address from, uint256 id, uint256 amount - ) internal view virtual override { - require(burner == from || _stakes[from][id][burner] >= amount, "ERC1238Stakable: Unauthorized to burn tokens"); + ) internal virtual override { + if (burner != from) { + require(_stakes[from][id][burner] >= amount, "ERC1238Stakable: Unauthorized to burn tokens"); + _decreaseStakeFrom(burner, from, id, amount); + } } /** * @dev Allows token owners to put their tokens at stake * * Calling this function again with the same stakeholder and id - * overrides the previous given allowance - * - * Requirements: - * + * adds to the previous staked amount * */ function _increaseStake( @@ -38,24 +53,33 @@ abstract contract ERC1238Stakable is ERC1238 { _stakes[msg.sender][id][stakeholder] += amount; } + /** + * @dev Lets sender (stakeholder) decrease a staked `amount` of + * tokens with id `id` belonging to `owner` + * + * Requirements: + * + * - `amount` must be less that the current staked amount + */ function _decreaseStake( address owner, uint256 id, uint256 amount ) internal { - uint256 authorization = _stakes[owner][id][msg.sender]; - - require(authorization >= amount, "ERC1238Stakable: cannot decrease more than current stake"); - unchecked { - _stakes[owner][id][msg.sender] = authorization - amount; - } + _decreaseStakeFrom(msg.sender, owner, id, amount); } - function stakeOf( + function _decreaseStakeFrom( + address stakeholder, address owner, uint256 id, - address stakeholder - ) public view returns (uint256) { - return _stakes[owner][id][stakeholder]; + uint256 amount + ) private { + uint256 authorization = _stakes[owner][id][stakeholder]; + + require(authorization >= amount, "ERC1238Stakable: cannot decrease more than current stake"); + unchecked { + _stakes[owner][id][stakeholder] = authorization - amount; + } } } diff --git a/contracts/mocks/ERC1238StakableMock.sol b/contracts/mocks/ERC1238StakableMock.sol index 78aeda3..fb2b244 100644 --- a/contracts/mocks/ERC1238StakableMock.sol +++ b/contracts/mocks/ERC1238StakableMock.sol @@ -16,7 +16,7 @@ contract ERC1238StakableMock is ERC1238, ERC1238Stakable { address from, uint256 id, uint256 amount - ) internal view override(ERC1238, ERC1238Stakable) { + ) internal override(ERC1238, ERC1238Stakable) { super._beforeBurn(burner, from, id, amount); } From c1e9197f02eaf0106784ed4bf17ff1e639f6d68b Mon Sep 17 00:00:00 2001 From: ra-phael <10075759+ra-phael@users.noreply.github.com> Date: Wed, 2 Feb 2022 21:56:48 +0000 Subject: [PATCH 8/9] add test --- test/ERC1238/extensions/ERC1238Stakable.ts | 27 +++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/test/ERC1238/extensions/ERC1238Stakable.ts b/test/ERC1238/extensions/ERC1238Stakable.ts index b93a8c6..bff4db8 100644 --- a/test/ERC1238/extensions/ERC1238Stakable.ts +++ b/test/ERC1238/extensions/ERC1238Stakable.ts @@ -1,8 +1,7 @@ -import { artifacts, ethers, waffle } from "hardhat"; -import type { Artifact } from "hardhat/types"; import type { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; import { expect } from "chai"; - +import { artifacts, ethers, waffle } from "hardhat"; +import type { Artifact } from "hardhat/types"; import type { ERC1238StakableMock } from "../../../src/types/ERC1238StakableMock"; import { toBN } from "../../utils/test-utils"; @@ -86,6 +85,28 @@ describe("ERC1238URIStakable", function () { ).to.be.revertedWith("ERC1238Stakable: Unauthorized to burn tokens"); }); + it("should not let a stakeholder burn more than its stake in several transactions", async () => { + // Given + const stakedAmount = toBN("500"); + // burnAmount is half the staked amount + const burnAmount = stakedAmount.div(2); + + await erc1238Stakable.connect(tokenOwner).increaseStake(stakeholder.address, fungibleTokenId, stakedAmount); + // burn half + await erc1238Stakable.connect(stakeholder).burn(tokenOwner.address, fungibleTokenId, burnAmount); + // burn the other half + await erc1238Stakable.connect(stakeholder).burn(tokenOwner.address, fungibleTokenId, burnAmount); + + expect(await erc1238Stakable.stakeOf(tokenOwner.address, fungibleTokenId, stakeholder.address)).to.eq(0); + + // When + // Tries to burn more + const tx = erc1238Stakable.connect(stakeholder).burn(tokenOwner.address, fungibleTokenId, burnAmount); + + // Expect + await expect(tx).to.be.revertedWith("ERC1238Stakable: Unauthorized to burn tokens"); + }); + it("should let a token owner burn tokens before staking", async () => { // Given expect(await erc1238Stakable.balanceOf(tokenOwner.address, fungibleTokenId)).to.eq(amountMintedFungible); From 2b1295168c9502ce1be24a44918602109c9c67f7 Mon Sep 17 00:00:00 2001 From: ra-phael <10075759+ra-phael@users.noreply.github.com> Date: Sun, 13 Feb 2022 11:52:30 -0700 Subject: [PATCH 9/9] update rules for burning --- .../ERC1238/extensions/ERC1238Holdable.sol | 1 - test/ERC1238/extensions/ERC1238Holdable.ts | 19 ++++++++++++++++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/contracts/ERC1238/extensions/ERC1238Holdable.sol b/contracts/ERC1238/extensions/ERC1238Holdable.sol index ea6cdb1..d6f743c 100644 --- a/contracts/ERC1238/extensions/ERC1238Holdable.sol +++ b/contracts/ERC1238/extensions/ERC1238Holdable.sol @@ -34,7 +34,6 @@ abstract contract ERC1238Holdable is IERC1238Holdable, ERC1238 { uint256 id, uint256 amount ) internal virtual override { - require(burner == from, "ERC1238Holdable: Unauthorized to burn tokens"); require(_escrowedBalances[burner][id] >= amount, "ERC1238Holdable: Amount to burn exceeds amount held"); _escrowedBalances[burner][id] -= amount; diff --git a/test/ERC1238/extensions/ERC1238Holdable.ts b/test/ERC1238/extensions/ERC1238Holdable.ts index 71d30bd..783791b 100644 --- a/test/ERC1238/extensions/ERC1238Holdable.ts +++ b/test/ERC1238/extensions/ERC1238Holdable.ts @@ -166,12 +166,25 @@ describe("ERC1238URIHoldable", function () { expect(await erc1238Holdable.balanceOf(tokenOwner.address, tokenId)).to.eq(mintAmount - amountToBurn); }); - it("should not give a token holder the right to burn tokens", async () => { + it("should give a token holder the right to burn tokens", async () => { + const amountToBurn = mintAmount - 100; + await erc1238Holdable.connect(tokenOwner).entrust(tokenHolder1.address, tokenId, mintAmount); + await erc1238Holdable.connect(tokenHolder1).burn(tokenOwner.address, tokenId, amountToBurn); + + expect(await erc1238Holdable.escrowedBalance(tokenHolder1.address, tokenId)).to.eq(mintAmount - amountToBurn); + expect(await erc1238Holdable.balanceOf(tokenOwner.address, tokenId)).to.eq(mintAmount - amountToBurn); + }); + + it("should not let a token holder burn more tokens than they hodl", async () => { + const escrowedAmount = mintAmount; + + await erc1238Holdable.connect(tokenOwner).entrust(tokenHolder1.address, tokenId, escrowedAmount); + await expect( - erc1238Holdable.connect(tokenHolder1).burn(tokenOwner.address, tokenId, mintAmount), - ).to.be.revertedWith("ERC1238Holdable: Unauthorized to burn tokens"); + erc1238Holdable.connect(tokenOwner).burn(tokenOwner.address, tokenId, escrowedAmount + 1), + ).to.be.revertedWith("ERC1238Holdable: Amount to burn exceeds amount held"); }); it("should not let a token owner burn tokens they do not hold", async () => {