diff --git a/contracts/ERC1238/extensions/ERC1238Holdable.sol b/contracts/ERC1238/extensions/ERC1238Holdable.sol new file mode 100644 index 0000000..d6f743c --- /dev/null +++ b/contracts/ERC1238/extensions/ERC1238Holdable.sol @@ -0,0 +1,64 @@ +// 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(_escrowedBalances[burner][id] >= amount, "ERC1238Holdable: Amount to burn exceeds amount held"); + + _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, + 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/ERC1238Stakable.sol b/contracts/ERC1238/extensions/ERC1238Stakable.sol new file mode 100644 index 0000000..cba3265 --- /dev/null +++ b/contracts/ERC1238/extensions/ERC1238Stakable.sol @@ -0,0 +1,85 @@ +// 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 => 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 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 + * adds to the previous staked amount + * + */ + function _increaseStake( + address stakeholder, + uint256 id, + uint256 amount + ) internal { + _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 { + _decreaseStakeFrom(msg.sender, owner, id, amount); + } + + function _decreaseStakeFrom( + address stakeholder, + address owner, + uint256 id, + 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/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/contracts/mocks/ERC1238StakableMock.sol b/contracts/mocks/ERC1238StakableMock.sol new file mode 100644 index 0000000..fb2b244 --- /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 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/ERC1238Holdable.ts b/test/ERC1238/extensions/ERC1238Holdable.ts new file mode 100644 index 0000000..783791b --- /dev/null +++ b/test/ERC1238/extensions/ERC1238Holdable.ts @@ -0,0 +1,202 @@ +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"; + +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 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 balance held 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 some of 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 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(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 () => { + 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"); + }); + }); +}); diff --git a/test/ERC1238/extensions/ERC1238Stakable.ts b/test/ERC1238/extensions/ERC1238Stakable.ts new file mode 100644 index 0000000..bff4db8 --- /dev/null +++ b/test/ERC1238/extensions/ERC1238Stakable.ts @@ -0,0 +1,193 @@ +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"; + +const BASE_URI = "https://token-cdn-domain/{id}.json"; + +describe("ERC1238URIStakable", function () { + let erc1238Stakable: ERC1238StakableMock; + let admin: SignerWithAddress; + let tokenOwner: SignerWithAddress; + let stakeholder: SignerWithAddress; + + before(async function () { + const signers: SignerWithAddress[] = await ethers.getSigners(); + admin = signers[0]; + tokenOwner = 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(tokenOwner.address, fungibleTokenId, amountMintedFungible, []); + // mint an NFT + await erc1238Stakable.connect(admin).mint(tokenOwner.address, nftID, amountMintedNonFungible, []); + }); + + it("should let a token owner increase a stake", async () => { + await erc1238Stakable.connect(tokenOwner).increaseStake(stakeholder.address, nftID, 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(tokenOwner.address, nftID)).to.eq(amountMintedNonFungible); + + // When + await erc1238Stakable.connect(tokenOwner).increaseStake(stakeholder.address, nftID, 1); + + await erc1238Stakable.connect(stakeholder).burn(tokenOwner.address, nftID, 1); + + // Expect + 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(tokenOwner.address, fungibleTokenId)).to.eq(amountMintedFungible); + + // When + await erc1238Stakable.connect(tokenOwner).increaseStake(stakeholder.address, fungibleTokenId, stakedAmount); + + await erc1238Stakable.connect(stakeholder).burn(tokenOwner.address, fungibleTokenId, stakedAmount); + + // Expect + expect(await erc1238Stakable.balanceOf(tokenOwner.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(tokenOwner).increaseStake(stakeholder.address, fungibleTokenId, stakedAmount); + + await expect( + erc1238Stakable.connect(stakeholder).burn(tokenOwner.address, fungibleTokenId, burnAmount), + ).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); + + // When + await erc1238Stakable.connect(tokenOwner).burn(tokenOwner.address, fungibleTokenId, amountMintedFungible); + + // Expect + 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(tokenOwner.address, fungibleTokenId)).to.eq(amountMintedFungible); + + // When + await erc1238Stakable.connect(tokenOwner).increaseStake(stakeholder.address, fungibleTokenId, stakedAmount); + + await erc1238Stakable.connect(tokenOwner).burn(tokenOwner.address, fungibleTokenId, stakedAmount); + + // Expect + expect(await erc1238Stakable.balanceOf(tokenOwner.address, fungibleTokenId)).to.eq( + amountMintedFungible.sub(stakedAmount), + ); + // "Burn" allowance is the same + expect(await erc1238Stakable.stakeOf(tokenOwner.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(tokenOwner).increaseStake(stakeholder.address, fungibleTokenId, stakedAmount); + + expect(await erc1238Stakable.stakeOf(tokenOwner.address, fungibleTokenId, stakeholder.address)).to.eq( + stakedAmount, + ); + + await erc1238Stakable.connect(stakeholder).decreaseStake(tokenOwner.address, fungibleTokenId, amountToUnstake); + + expect(await erc1238Stakable.stakeOf(tokenOwner.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(tokenOwner).increaseStake(stakeholder.address, fungibleTokenId, stakedAmount); + + expect(await erc1238Stakable.stakeOf(tokenOwner.address, fungibleTokenId, stakeholder.address)).to.eq( + stakedAmount, + ); + + await erc1238Stakable.connect(stakeholder).decreaseStake(tokenOwner.address, fungibleTokenId, amountToUnstake); + + 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(tokenOwner).increaseStake(stakeholder.address, fungibleTokenId, stakedAmount); + + expect(await erc1238Stakable.stakeOf(tokenOwner.address, fungibleTokenId, stakeholder.address)).to.eq( + stakedAmount, + ); + + await expect( + erc1238Stakable.connect(tokenOwner).decreaseStake(tokenOwner.address, fungibleTokenId, stakedAmount), + ).to.be.revertedWith("ERC1238Stakable: cannot decrease more than current stake"); + + expect(await erc1238Stakable.stakeOf(tokenOwner.address, fungibleTokenId, stakeholder.address)).to.eq( + stakedAmount, + ); + }); + }); + }); +});