Contract Source Code:
pragma solidity 0.8.17;
// SPDX-License-Identifier: AGPL-3.0-only
import "./AgentNFT.sol";
import "./lib/TransferHelper.sol";
/// @title Referral system contract
/// @notice If a user deposits in Pair and has registered in referral system,
/// he will get extra DYSON token as reward.
/// Each user in the referral system is an `Agent`.
/// Referral of a agent is called the `child` of the agent.
contract Agency {
using TransferHelper for address;
bytes32 public constant REGISTER_ONCE_TYPEHASH = keccak256("register(address child)"); // onceSig
bytes32 public constant REGISTER_PARENT_TYPEHASH = keccak256("register(address once,uint256 deadline,uint256 price)"); // parentSig
/// @notice Max number of children, i.e., referrals, per agent
/// Note that this limit is not forced on root agent
uint constant MAX_NUM_CHILDREN = 3;
/// @notice Amount of time a new agent have to wait before he can refer a new user
uint constant REGISTER_DELAY = 4 hours;
/// @notice Cooldown time before an agent can transfer his agent
uint constant TRANSFER_CD = 60000;
AgentNFT public immutable agentNFT;
/// @dev For EIP-2612 permit
bytes32 public immutable DOMAIN_SEPARATOR;
/// @member owner Owner of the agent data
/// @member gen Agent's generation in the referral system
/// @member birth Timestamp when the agent registerred in the referral system
/// @member parentId Id of the agent's parent, i.e., it's referrer
/// @member childrenId Ids of the agent's children, i.e., referrals
struct Agent {
address owner;
uint gen;
uint birth;
uint parentId;
uint[] childrenId;
}
address public owner;
/// @notice Number of users in the referral system
uint public totalSupply;
/// @notice User's id in the referral system
/// Param is User's address
mapping(address => uint) public whois;
/// @notice User's agent
/// Param is User's id in the referral system
mapping(uint => Agent) internal agents;
/// @notice Record the time when a user can transfer his agent
mapping(uint => uint) public transferCooldown;
/// @notice Record if an invite code has been used
mapping(address => bool) public oneTimeCodes;
/// @notice Record if a hash has been presigned by an address
mapping(address => mapping(bytes32 => bool)) public presign;
/// @notice Record if an address is a controller
mapping (address => bool) public isController;
event TransferOwnership(address newOwner);
event Register(uint indexed referrer, uint referee);
event Sign(address indexed signer, bytes32 digest);
constructor(address _owner, address root) {
require(_owner != address(0), "invalid owner");
DOMAIN_SEPARATOR = keccak256(abi.encode(
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
keccak256(bytes("Agency")),
keccak256(bytes("1")),
block.chainid,
address(this)
));
owner = _owner;
AgentNFT _agentNFT = new AgentNFT(address(this));
agentNFT = _agentNFT;
// Initialize root
uint id = ++totalSupply; // root agent has id 1
whois[root] = id;
Agent storage rootAgent = agents[id];
rootAgent.owner = root;
rootAgent.birth = block.timestamp;
rootAgent.parentId = id; // root agent's parent is also root agent itself
_agentNFT.onMint(root, id);
emit Register(id, id);
}
modifier onlyOwner() {
require(msg.sender == owner, "forbidden");
_;
}
function transferOwnership(address _owner) external onlyOwner {
require(_owner != address(0), "owner cannot be zero");
owner = _owner;
emit TransferOwnership(_owner);
}
/// @notice rescue token stucked in this contract
/// @param tokenAddress Address of token to be rescued
/// @param to Address that will receive token
/// @param amount Amount of token to be rescued
function rescueERC20(address tokenAddress, address to, uint256 amount) onlyOwner external {
tokenAddress.safeTransfer(to, amount);
}
function addController(address _controller) external onlyOwner {
isController[_controller] = true;
}
function removeController(address _controller) external onlyOwner {
isController[_controller] = false;
}
/// @notice Add new child agent to root agent. This child will have the privilege of being a 1st generation agent.
/// This function can only be executed by admin, which is either `owner` or `controller`.
/// @param newUser User of the new agent
/// @return id Id of the new agent
function adminAdd(address newUser) external returns (uint id) {
require(msg.sender == owner || isController[msg.sender], "forbidden");
require(whois[newUser] == 0, "occupied");
id = _newAgent(newUser, 1);
}
/// @notice Transfer agent data to another user
/// Can not transfer to a user who already has an agent.
/// @param from previous owner of the agent
/// @param to User who will receive the agent
/// @param id index of the agent to be transfered
/// @return True if transfer succeed
function transfer(address from, address to, uint id) external returns (bool) {
require(msg.sender == address(agentNFT), "forbidden");
require(to != address(0), "transfer invalid address");
require(id != 0, "nothing to transfer");
require(id == whois[from], "forbidden");
require(transferCooldown[id] < block.timestamp, "cd");
Agent storage agent = agents[id];
require(whois[to] == 0, "occupied");
agent.owner = to;
whois[to] = id;
whois[from] = 0;
// agent can not be transfered again until cooldown time, (gen + 1) * TRANSFER_CD,
// which is 10 times as long as the cooldown time of swapping SP to DYSON
transferCooldown[id] = block.timestamp + (agent.gen + 1) * TRANSFER_CD;
return true;
}
/// @dev Create new `Agent` data and update the link between the agent and it's parent agent
function _newAgent(address _owner, uint parentId) internal returns (uint id) {
require(_owner != address(0), "new agent invalid address");
id = ++totalSupply;
whois[_owner] = id;
Agent storage parent = agents[parentId];
Agent storage child = agents[id];
parent.childrenId.push(id);
child.owner = _owner;
child.gen = parent.gen + 1;
child.birth = block.timestamp;
child.parentId = parentId;
agentNFT.onMint(_owner, id);
emit Register(parentId, id);
}
function _getHashTypedData(bytes32 structHash) internal view returns (bytes32) {
return keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR, structHash));
}
/// @notice User register in the referral system by providing an one time invite code: `onceSig`
/// and his referrer's signature: `parentSig`.
/// User can not register if he already has an agent.
/// User can not register if the referrer already has maximum number of child agents.
/// User can not register if the referrer is new and has not passed the register delay
/// @notice If the referral code is presigned, use parent's address for parentSig
/// @param parentSig Referrer's signature or referrer's address
/// @param onceSig Invite code
/// @param deadline Deadline of the invite code, set by the referrer
/// @return id Id of the new agent
function register(bytes memory parentSig, bytes memory onceSig, uint deadline) payable external returns (uint id) {
require(block.timestamp < deadline, "exceed deadline");
require(whois[msg.sender] == 0, "already registered");
bytes32 onceSigDigest = _getHashTypedData(keccak256(abi.encode(
REGISTER_ONCE_TYPEHASH,
msg.sender
)));
address once = _ecrecover(onceSigDigest, onceSig);
require(once != address(0), "invalid once sig");
require(oneTimeCodes[once] == false, "signature is used");
bytes32 parentSigDigest = _getHashTypedData(keccak256(abi.encode(
REGISTER_PARENT_TYPEHASH,
once,
deadline,
msg.value
)));
address _parent;
if(parentSig.length == 65) {
_parent = _ecrecover(parentSigDigest, parentSig);
}
else if(parentSig.length == 20) {
assembly {
_parent := mload(add(parentSig, 20))
}
require(presign[_parent][parentSigDigest], "invalid parent sig");
}
require(_parent != address(0), "invalid parent sig");
uint parentId = whois[_parent];
require(parentId != 0, "invalid parent");
Agent storage parent = agents[parentId];
require(parent.childrenId.length < MAX_NUM_CHILDREN, "no empty slot");
require(parent.birth + REGISTER_DELAY <= block.timestamp, "not ready");
id = _newAgent(msg.sender, parentId);
oneTimeCodes[once] = true;
if(msg.value > 0) {
_parent.safeTransferETH(msg.value);
}
}
/// @dev parent do onchain presign for a referral code
function sign(bytes32 digest) external {
presign[msg.sender][digest] = true;
emit Sign(msg.sender, digest);
}
function getHashTypedData(bytes32 structHash) external view returns (bytes32) {
return _getHashTypedData(structHash);
}
/// @notice User's agent data
/// @param _owner User's address
/// @return ref Parent agent's owner address
/// @return gen Generation of user's agent
function userInfo(address _owner) external view returns (address ref, uint gen) {
Agent storage agent = agents[whois[_owner]];
ref = agents[agent.parentId].owner;
gen = agent.gen;
}
/// @notice Get agent data by user's id
/// @param id Id of the user
/// @return User's agent data
function getAgent(uint id) external view returns (address, uint, uint, uint, uint[] memory) {
Agent storage agent = agents[id];
return(agent.owner, agent.gen, agent.birth, agent.parentId, agent.childrenId);
}
function _ecrecover(bytes32 hash, bytes memory signature) internal pure returns (address) {
if (signature.length == 65) {
bytes32 r;
bytes32 s;
uint8 v;
assembly {
r := mload(add(signature, 0x20))
s := mload(add(signature, 0x40))
v := byte(0, mload(add(signature, 0x60)))
}
if (uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) {
return address(0);
} else if (v != 27 && v != 28) {
return address(0);
} else {
return ecrecover(hash, v, r, s);
}
} else {
return address(0);
}
}
}
pragma solidity 0.8.17;
// SPDX-License-Identifier: AGPL-3.0-only
import "interface/IAgency.sol";
import "interface/IERC721Receiver.sol";
library Base64 {
bytes internal constant TABLE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
/// @notice Encodes some bytes to the base64 representation
function encode(bytes memory data) internal pure returns (string memory) {
uint len = data.length;
if (len == 0) return "";
// multiply by 4/3 rounded up
uint encodedLen = 4 * ((len + 2) / 3);
// Add some extra buffer at the end
bytes memory result = new bytes(encodedLen + 32);
bytes memory table = TABLE;
assembly {
let tablePtr := add(table, 1)
let resultPtr := add(result, 32)
for {
let i := 0
} lt(i, len) {
} {
i := add(i, 3)
let input := and(mload(add(data, i)), 0xffffff)
let out := mload(add(tablePtr, and(shr(18, input), 0x3F)))
out := shl(8, out)
out := add(out, and(mload(add(tablePtr, and(shr(12, input), 0x3F))), 0xFF))
out := shl(8, out)
out := add(out, and(mload(add(tablePtr, and(shr(6, input), 0x3F))), 0xFF))
out := shl(8, out)
out := add(out, and(mload(add(tablePtr, and(input, 0x3F))), 0xFF))
out := shl(224, out)
mstore(resultPtr, out)
resultPtr := add(resultPtr, 4)
}
switch mod(len, 3)
case 1 {
mstore(sub(resultPtr, 2), shl(240, 0x3d3d))
}
case 2 {
mstore(sub(resultPtr, 1), shl(248, 0x3d))
}
mstore(result, encodedLen)
}
return string(result);
}
}
contract AgentNFT {
IAgency public immutable agency;
/// @dev ERC165 interface ID of ERC165
bytes4 private constant ERC165_INTERFACE_ID = 0x01ffc9a7;
/// @dev ERC165 interface ID of ERC721
bytes4 private constant ERC721_INTERFACE_ID = 0x80ac58cd;
/// @dev ERC165 interface ID of ERC721Metadata
bytes4 private constant ERC721_METADATA_INTERFACE_ID = 0x5b5e139f;
/// @dev Get the approved address for a single NFT.
mapping(uint => address) public getApproved;
/// @dev Checks if an address is an approved operator.
mapping(address => mapping(address => bool)) public isApprovedForAll;
/**
* @dev Emitted when `tokenId` token is transferred from `from` to `to`.
*/
event Transfer(address indexed from, address indexed to, uint indexed tokenId);
/**
* @dev Emitted when `owner` enables `approved` to manage the `tokenId` token.
*/
event Approval(address indexed owner, address indexed approved, uint indexed tokenId);
/**
* @dev Emitted when `owner` enables or disables (`approved`) `operator` to manage all of its assets.
*/
event ApprovalForAll(address indexed owner, address indexed operator, bool approved);
constructor(address _agency) {
agency = IAgency(_agency);
}
/**
* @dev Interface identification is specified in ERC-165.
* @param interfaceID Id of the interface
*/
function supportsInterface(bytes4 interfaceID) external pure returns (bool) {
return (interfaceID == ERC165_INTERFACE_ID ||
interfaceID == ERC721_INTERFACE_ID ||
interfaceID == ERC721_METADATA_INTERFACE_ID);
}
function name() external pure returns (string memory) {
return "Agent";
}
function symbol() external pure returns (string memory) {
return "DAG";
}
function tokenURI(uint tokenId) external view returns (string memory) {
(address owner, uint tier, uint birth, uint parent,) = agency.getAgent(tokenId);
require(owner != address(0), "token not exist");
return _tokenURI(tokenId, parent, tier, birth);
}
function _tokenURI(uint tokenId, uint parent, uint tier, uint birth) internal pure returns (string memory output) {
output = '<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="10" y="20" class="base">';
output = string(abi.encodePacked(output, "token ", _toString(tokenId), '</text><text x="10" y="40" class="base">'));
output = string(abi.encodePacked(output, "referer ", _toString(parent), '</text><text x="10" y="60" class="base">'));
output = string(abi.encodePacked(output, "agent_tier ", _toString(tier), '</text><text x="10" y="80" class="base">'));
output = string(abi.encodePacked(output, "time_of_creation ", _toString(birth), '</text></svg>'));
string memory json = Base64.encode(bytes(string(abi.encodePacked('{"name": "Agent #', _toString(tokenId), '", "description": "Dyson Finance Agent NFT", "image": "data:image/svg+xml;base64,', Base64.encode(bytes(output)), '"}'))));
output = string(abi.encodePacked('data:application/json;base64,', json));
}
function _toString(uint value) internal pure returns (string memory) {
if (value == 0) {
return "0";
}
uint temp = value;
uint digits;
while (temp != 0) {
digits++;
temp /= 10;
}
bytes memory buffer = new bytes(digits);
while (value != 0) {
digits -= 1;
buffer[digits] = bytes1(uint8(48 + uint(value % 10)));
value /= 10;
}
return string(buffer);
}
function totalSupply() external view returns (uint) {
return agency.totalSupply();
}
function balanceOf(address owner) external view returns (uint balance) {
return agency.whois(owner) == 0 ? 0 : 1;
}
function ownerOf(uint tokenId) public view returns (address owner) {
(owner,,,,) = agency.getAgent(tokenId);
}
function onMint(address user, uint tokenId) external {
require(msg.sender == address(agency), "forbidden");
emit Transfer(address(0), user, tokenId);
}
function safeTransferFrom(
address from,
address to,
uint tokenId
) external {
safeTransferFrom(from, to, tokenId, '');
}
function approve(address to, uint tokenId) external {
address owner = ownerOf(tokenId);
// Throws if `tokenId` is not a valid NFT
require(owner != address(0), "token not exist");
// Check requirements
require(owner == msg.sender || isApprovedForAll[owner][msg.sender], "forbidden");
getApproved[tokenId] = to;
emit Approval(owner, to, tokenId);
}
function setApprovalForAll(address operator, bool approved) external {
require(operator != msg.sender, "self approval");
isApprovedForAll[msg.sender][operator] = approved;
emit ApprovalForAll(msg.sender, operator, approved);
}
function _transferFrom(
address from,
address to,
uint tokenId,
address sender
) internal {
require(from == sender || isApprovedForAll[from][sender] || getApproved[tokenId] == sender, "forbidden");
getApproved[tokenId] = address(0);
require(agency.transfer(from, to, tokenId), "forbidden");
emit Transfer(from, to, tokenId);
}
function transferFrom(
address from,
address to,
uint tokenId
) external {
_transferFrom(from, to, tokenId, msg.sender);
}
function _isContract(address account) internal view returns (bool) {
uint size;
assembly {
size := extcodesize(account)
}
return size > 0;
}
function safeTransferFrom(
address from,
address to,
uint tokenId,
bytes memory data
) public {
_transferFrom(from, to, tokenId, msg.sender);
if (_isContract(to)) {
try IERC721Receiver(to).onERC721Received(msg.sender, from, tokenId, data) returns (bytes4 retval) {
require(retval == IERC721Receiver.onERC721Received.selector, "transfer failed");
} catch (bytes memory reason) {
if (reason.length == 0) {
revert('ERC721: transfer to non ERC721Receiver implementer');
} else {
assembly {
revert(add(32, reason), mload(reason))
}
}
}
}
}
}
pragma solidity 0.8.17;
// SPDX-License-Identifier: AGPL-2.0
library TransferHelper {
function safeApprove(address token, address to, uint value) internal {
// bytes4(keccak256(bytes('approve(address,uint256)')));
(bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x095ea7b3, to, value));
require(success && (data.length == 0 || abi.decode(data, (bool))), 'transferHelper: approve failed');
}
function safeTransfer(address token, address to, uint value) internal {
// bytes4(keccak256(bytes('transfer(address,uint256)')));
(bool success, bytes memory data) = token.call(abi.encodeWithSelector(0xa9059cbb, to, value));
require(success && (data.length == 0 || abi.decode(data, (bool))), 'transferHelper: transfer failed');
}
function safeTransferFrom(address token, address from, address to, uint value) internal {
// bytes4(keccak256(bytes('transferFrom(address,address,uint256)')));
(bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x23b872dd, from, to, value));
require(success && (data.length == 0 || abi.decode(data, (bool))), 'transferHelper: transferFrom failed');
}
function safeTransferETH(address to, uint value) internal {
(bool success,) = to.call{value:value}(new bytes(0));
require(success, 'transferHelper: ETH transfer failed');
}
}
pragma solidity >=0.8.0;
// SPDX-License-Identifier: MIT
interface IAgency {
struct Agent {
address owner;
uint gen;
uint birth;
uint parentId;
uint[] childrenId;
}
event TransferOwnership(address newOwner);
event Register(uint indexed referrer, uint referee);
event Sign(address indexed signer, bytes32 digest);
function REGISTER_ONCE_TYPEHASH() external view returns (bytes32);
function REGISTER_PARENT_TYPEHASH() external view returns (bytes32);
function MAX_NUM_CHILDREN() external view returns (uint);
function REGISTER_DELAY() external view returns (uint);
function TRANSFER_CD() external view returns (uint);
function agentNFT() external view returns (address);
function DOMAIN_SEPARATOR() external view returns (bytes32);
function whois(address agent) external view returns (uint);
function oneTimeCodes(address once) external view returns (bool);
function presign(address agent, bytes32 digest) external view returns (bool);
function isController(address agent) external view returns (bool);
function owner() external view returns (address);
function userInfo(address agent) external view returns (address ref, uint gen);
function transfer(address from, address to, uint id) external returns (bool);
function totalSupply() external view returns (uint);
function getAgent(uint id) external view returns (address, uint, uint, uint, uint[] memory);
function transferOwnership(address owner) external;
function addController(address _controller) external;
function removeController(address _controller) external;
function rescueERC20(address tokenAddress, address to, uint256 amount) external;
function adminAdd(address newUser) external returns (uint id);
function register(bytes memory parentSig, bytes memory onceSig, uint deadline) payable external returns (uint id);
function sign(bytes32 digest) external;
function getHashTypedData(bytes32 structHash) external view returns (bytes32);
function transferCooldown(uint id) external view returns (uint);
}
pragma solidity >=0.8.0;
// SPDX-License-Identifier: MIT
/**
* @title ERC721 token receiver interface
* @dev Interface for any contract that wants to support safeTransfers
* from ERC721 asset contracts.
*/
interface IERC721Receiver {
function onERC721Received(address, address, uint, bytes calldata) external returns (bytes4);
}