Skip to content

Instantly share code, notes, and snippets.

@ngmachado
Last active October 6, 2022 14:32
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ngmachado/4e1349385fc239008761df8d19d2cdb4 to your computer and use it in GitHub Desktop.
Save ngmachado/4e1349385fc239008761df8d19d2cdb4 to your computer and use it in GitHub Desktop.
CFA Based NFT (onchain SVG)
pragma solidity ^0.8.0;
import { Strings } from "@openzeppelin/contracts/utils/Strings.sol";
import { Base64 } from "@openzeppelin/contracts/utils/Base64.sol";
import {IConstantFlowAgreementV1} from "@superfluid-finance/ethereum-contracts/contracts/interfaces/agreements/IConstantFlowAgreementV1.sol";
import { ISuperfluid, ISuperToken } from "@superfluid-finance/ethereum-contracts/contracts/interfaces/superfluid/ISuperfluid.sol";
/*
Super721SVGOnChain - Generate NFT based on SF CFA Stream
*/
contract Super721SVGOnChain {
using Strings for uint256;
// Superfluid struct that we get from hook - remove after SF integration
struct CFAHookParams {
address sender;
address receiver;
address flowOperator;
int96 flowRate;
}
struct StreamData {
address token;
address sender;
}
error NOT_ALLOWED();
error ALREADY_MINTED();
error NOT_MINTED();
error ZERO_ADDRESS();
error EMPTY_DATA();
error NOT_STREAM_USER();
event Transfer(address indexed from, address indexed to, uint256 indexed id);
string public name;
string public symbol;
IConstantFlowAgreementV1 public cfaV1;
uint256 private _tokenIds;
mapping(uint256 => StreamData) internal _tokenCFAData;
mapping(bytes32 => uint256) internal _revertStreamToId;
mapping(uint256 => address) internal _ownerOf;
function tokenURI(uint256 id) public view returns (string memory) {
StreamData memory stream = _tokenCFAData[id];
address receiver = ownerOf(id);
int96 flowRate = _getFlowRate(stream.token, stream.sender, receiver);
bytes memory dataURI = abi.encodePacked(
'{',
'"name": "CFA NFT: ', id.toString(), '",',
'"description": "Represents a stream on chain",',
'"image": "', _generateSVG(stream.token, flowRate, stream.sender, receiver), '"',
'}'
);
return string(
abi.encodePacked(
"data:application/json;base64,",
Base64.encode(dataURI)
)
);
}
function _generateSVG(
address token,
int96 flowRate,
address sender,
address receiver
) internal view returns(string memory) {
bytes memory svg = abi.encodePacked(
'<svg xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMin meet" viewBox="0 0 350 350">',
'<style>.base { fill: white; font-family: serif; font-size: 14px; }</style>',
'<rect width="100%" height="100%" fill="black" />',
'<text x="50%" y="40%" class="base" dominant-baseline="middle" text-anchor="middle">', "Token: ", Strings.toHexString(uint256(uint160(token)), 20),'</text>',
'<text x="50%" y="50%" class="base" dominant-baseline="middle" text-anchor="middle">', "Sender: ", Strings.toHexString(uint256(uint160(sender)), 20),'</text>',
'<text x="50%" y="60%" class="base" dominant-baseline="middle" text-anchor="middle">', "Receiver: ", Strings.toHexString(uint256(uint160(receiver)), 20),'</text>',
'<text x="50%" y="70%" class="base" dominant-baseline="middle" text-anchor="middle">', "FlowRate: ", uint256(uint96(flowRate)).toString(),'</text>',
'</svg>'
);
return string(
abi.encodePacked(
"data:image/svg+xml;base64,",
Base64.encode(svg)
)
);
}
constructor(address cfa, string memory _name, string memory _symbol) {
cfaV1 = IConstantFlowAgreementV1(cfa);
name = _name;
symbol = _symbol;
}
function ownerOf(uint256 id) public view virtual returns (address owner) {
owner = _ownerOf[id];
if(owner == address(0)) revert NOT_MINTED();
}
function balanceOf(address owner) public view virtual returns (uint256) {
return 1;
}
function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) {
return
interfaceId == 0x01ffc9a7 || // ERC165 Interface ID for ERC165
interfaceId == 0x80ac58cd || // ERC165 Interface ID for ERC721
interfaceId == 0x5b5e139f; // ERC165 Interface ID for ERC721Metadata
}
//Mock until SF integration
function onCreate(CFAHookParams memory newFlowData, address token) public returns(bool) {
_tokenIds++;
_mint(_tokenIds, token, newFlowData.sender, newFlowData.receiver);
return true;
}
function onUpdate(CFAHookParams memory updatedFlowData, address token, int96 oldFlowRate) public returns(bool) {
return true;
}
function onDelete(CFAHookParams memory updatedFlowData, address token, int96 oldFlowRate) public returns(bool) {
_burn(token, updatedFlowData.sender, updatedFlowData.receiver);
return true;
}
function _mint(uint256 id, address token, address sender, address receiver) internal {
if(receiver == address(0)) revert ZERO_ADDRESS();
bytes32 reverseKey = keccak256(abi.encodePacked(
token,
sender,
receiver
));
if(_revertStreamToId[reverseKey] != 0) revert ALREADY_MINTED();
if(_ownerOf[id] != address(0)) revert ALREADY_MINTED();
_ownerOf[id] = receiver;
_tokenCFAData[id] = StreamData(token, sender);
_revertStreamToId[reverseKey] = id;
emit Transfer(address(0), receiver, id);
}
function _burn(address token, address sender, address receiver) internal {
bytes32 reverseKey = keccak256(abi.encodePacked(token, sender, receiver));
uint256 id = _revertStreamToId[reverseKey];
if(id == 0) revert NOT_MINTED();
delete _ownerOf[id];
delete _tokenCFAData[id];
delete _revertStreamToId[reverseKey];
emit Transfer(receiver, address(0), id);
}
function mint(address token, address sender, address receiver) public {
int96 flowRate = _getFlowRate(token, sender, receiver);
if(flowRate > 0) {
_tokenIds++;
_mint(_tokenIds, token, sender, receiver);
}
}
function burn(address token, address sender, address receiver) public {
if(msg.sender != receiver) revert NOT_STREAM_USER();
int96 flowRate = _getFlowRate(token, sender, receiver);
if(flowRate == 0) {
_burn(token, sender, receiver);
}
}
function approve(address spender, uint256 id) public {
revert NOT_ALLOWED();
}
function setApprovalForAll(address operator, bool approved) public {
revert NOT_ALLOWED();
}
function transferFrom(address from,address to,uint256 id) public {
revert NOT_ALLOWED();
}
function safeTransferFrom(address from,address to,uint256 id) public {
revert NOT_ALLOWED();
}
function safeTransferFrom(address from, address to, uint256 id,bytes calldata data) public {
revert NOT_ALLOWED();
}
function _getFlowRate(address token, address sender, address receiver) internal view returns(int96 flowRate) {
(,flowRate,,) = cfaV1.getFlow(ISuperToken(token), sender, receiver);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment