Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions contracts/ERC1238/extensions/ERC1238Holdable.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "../ERC1238.sol";
import "./IERC1238Holdable.sol";
import "./IERC1238Holder.sol";

/**
* @dev Proposal for ERC1238 tokens extension that allow addresses
* to hold tokens on behalf of others.
*/
abstract contract ERC1238Holdable is IERC1238Holdable, ERC1238 {
using Address for address;

// Mapping holder => id => balance
mapping(address => mapping(uint256 => uint256)) private _heldBalances;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add another mapping for discoverability?

As a user/application I would want an easy way to check who might be a holder of a particular token. This mapping just gives us the balances of a specific holder we might be interested in.


function heldBalance(address holder, uint256 id) public view override returns (uint256) {
return _heldBalances[holder][id];
}

/**
* @dev Hooks into the minting flow to set the token recipient as first holder
* by default when tokens are minted.
*/
function _beforeMint(
address,
address to,
uint256 id,
uint256 amount,
bytes memory
) internal virtual override {
_heldBalances[to][id] += amount;
}

/**
* @dev Burns `amount` of tokens that are held by `holder` and owned by `from`.
* If `holder` is a smart contract and inherits {IERC1238Holder}, it notifies it to give it a chance to
* react to the burn and handle the operation how it sees fit.
*
* Requirements:
* - `holder` should hold at least the `amount` of tokens with the `id` passed
*/
function _burnHeldTokens(
Copy link
Copy Markdown

@0xpApaSmURf 0xpApaSmURf Apr 14, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would still just call this a burn function. A burn may affect a token that is currently being held but it also may not. It should just attempt burn flows, check if its being held, if it is, then do more, if not, continue with burn flow.

address burner,
address holder,
address from,
uint256 id,
uint256 amount
) internal virtual {
require(_heldBalances[holder][id] >= amount, "ERC1238Holdable: Amount to burn exceeds amount held");

if (holder.isContract()) {
try IERC1238Holder(holder).onBurn(id, amount) returns (bool isBurnAcknowledged) {
if (!isBurnAcknowledged) emit BurnAcknowledgmentFailed(holder, burner, from, id, amount);
} catch {
emit BurnAcknowledgmentFailed(holder, burner, from, id, amount);
}
}

super._burn(from, id, amount);

_heldBalances[holder][id] -= amount;
}

/**
* @dev Lets sender entrusts `to` with `amount`
* of tokens which gets transferred between their respective heldBalances
*/
function _entrust(
address to,
uint256 id,
uint256 amount
) internal virtual {
address from = msg.sender;

uint256 fromBalance = _heldBalances[from][id];
require(fromBalance >= amount, "ERC1238Holdable: amount exceeds balance held");

_heldBalances[from][id] -= amount;
_heldBalances[to][id] += amount;

emit Entrust(from, to, id, amount);
}

// TODO: Add a function to provide a safer alternative which
// makes sure the recipient is a IERC1238Holder contract (same as idea as in IERC1238Receiver)
}
37 changes: 37 additions & 0 deletions contracts/ERC1238/extensions/IERC1238Holdable.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "../IERC1238.sol";

/**
* @dev Proposal of an interface for ERC1238 tokens that can be held by another address than
* than their owner or staked in a smart contract.
*/
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 Event emitted when tokens are burnt and the holder fails to acknowledge the burn.
*/
event BurnAcknowledgmentFailed(address holder, address burner, address from, uint256 indexed id, uint256 amount);

/**
* @dev Returns the balance of a token holder for a given `id`.
*/
function heldBalance(address holder, uint256 id) external view returns (uint256);

/**
* @dev Lets sender entrusts `to` with `amount`
* of tokens which gets transferred between their respective balances
* of tokens held.
*/
function entrust(
address to,
uint256 id,
uint256 amount
) external;
}
15 changes: 15 additions & 0 deletions contracts/ERC1238/extensions/IERC1238Holder.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "../IERC1238.sol";
import "../IERC1238Receiver.sol";

/**
* @dev Interface proposal for contracts that need to hold ERC1238 tokens.
*/
interface IERC1238Holder is IERC1238Receiver {
/**
* @dev This function is called when tokens with id `id` are burnt.
*/
function onBurn(uint256 id, uint256 amount) external returns (bool);
}
66 changes: 66 additions & 0 deletions contracts/mocks/ERC1238HoldableMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// 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 mintToContract(
address to,
uint256 id,
uint256 amount,
bytes memory data
) public {
_mintToContract(to, id, amount, data);
}

function mintBatchToContract(
address to,
uint256[] memory ids,
uint256[] memory amounts,
bytes memory data
) public {
_mintBatchToContract(to, ids, amounts, data);
}

function burnHeldTokens(
address holder,
address from,
uint256 id,
uint256 amount
) public {
_burnHeldTokens(msg.sender, holder, from, 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);
}
}
65 changes: 65 additions & 0 deletions contracts/mocks/ERC1238HolderMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "../ERC1238/IERC1238Receiver.sol";
import "../ERC1238/extensions/IERC1238Holdable.sol";
import "../ERC1238/extensions/IERC1238Holder.sol";

import "./ERC1238HoldableMock.sol";

// This is a dummy example of a ERC1238Holder with arbitrary rules.
// It will reject non-transferable tokens if the token id is 0 in the case of a single token mint
// or if the first token id is 0 for a batch mint.
//
// It will also acknowledge and emit an event when tokens are burnt.
contract ERC1238HolderMock is IERC1238Receiver, IERC1238Holder {
// bytes4(keccak256("onERC1238Mint(address,uint256,uint256,bytes)"))
bytes4 public constant ERC1238_ON_MINT = 0x45ed75d5;

// bytes4(keccak256("onERC1238BatchMint(address,uint256[],uint256[],bytes)"))
bytes4 public constant ERC1238_ON_BATCH_MINT = 0xc0bfec68;

event TokenBurnt(uint256 id, uint256 amount);

function onERC1238Mint(
address,
uint256 id,
uint256,
bytes calldata
) external pure override returns (bytes4) {
if (id == 0) {
return bytes4(0);
}

return ERC1238_ON_MINT;
}

function onERC1238BatchMint(
address,
uint256[] calldata ids,
uint256[] calldata,
bytes calldata
) external pure override returns (bytes4) {
if (ids[0] == 0) {
return bytes4(0);
}

return ERC1238_ON_BATCH_MINT;
}

function entrust(
address targetContract,
address to,
uint256 id,
uint256 amount
) external {
IERC1238Holdable(targetContract).entrust(to, id, amount);
}

function onBurn(uint256 id, uint256 amount) public override returns (bool) {
emit TokenBurnt(id, amount);

return true;
}
}
63 changes: 63 additions & 0 deletions contracts/mocks/ERC1238ReceiverHoldableMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "../ERC1238/IERC1238Receiver.sol";
import "../ERC1238/extensions/IERC1238Holdable.sol";

import "./ERC1238HoldableMock.sol";

// This is a dummy example of a ERC1238Receiver with arbitrary rules.
// It will reject non-transferable tokens if the token id is 0 in the case of a single token mint
// or if the first token id is 0 for a batch mint.
contract ERC1238ReceiverHoldableMock is IERC1238Receiver {
// bytes4(keccak256("onERC1238Mint(address,uint256,uint256,bytes)"))
bytes4 public constant ERC1238_ON_MINT = 0x45ed75d5;

// bytes4(keccak256("onERC1238BatchMint(address,uint256[],uint256[],bytes)"))
bytes4 public constant ERC1238_ON_BATCH_MINT = 0xc0bfec68;

function onERC1238Mint(
address,
uint256 id,
uint256,
bytes calldata
) external pure override returns (bytes4) {
if (id == 0) {
return bytes4(0);
}

return ERC1238_ON_MINT;
}

function onERC1238BatchMint(
address,
uint256[] calldata ids,
uint256[] calldata,
bytes calldata
) external pure override returns (bytes4) {
if (ids[0] == 0) {
return bytes4(0);
}

return ERC1238_ON_BATCH_MINT;
}

function entrust(
address targetContract,
address to,
uint256 id,
uint256 amount
) external {
IERC1238Holdable(targetContract).entrust(to, id, amount);
}

function burnHeldTokens(
address targetContract,
address holder,
uint256 id,
uint256 amount
) external {
ERC1238HoldableMock(targetContract).burnHeldTokens(holder, address(this), id, amount);
}
}
Loading