SKIP TO CONTENT

Demo build — mock protocol data. No chain, no ABIs yet.

  • SYNCING: ……

K401Seat6551.sol

ERC-6551 token bound accounts for Seats. Lazily deployed on first stock delivery. 0 fee to withdraw, ever.

195 lines7.5 KBSolidity
Source of src/K401Seat6551.sol, 195 lines of Solidity
1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.24;
3 
4import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
5import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
6import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
7import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
8import {IERC1271} from "@openzeppelin/contracts/interfaces/IERC1271.sol";
9import {SignatureChecker} from "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol";
10import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
11import {IERC6551Registry, IK401} from "./interfaces/IK401Interfaces.sol";
12 
13/*//////////////////////////////////////////////////////////////
14 ACCOUNT IMPLEMENTATION
15//////////////////////////////////////////////////////////////*/
16 
17/**
18 * @title K401Seat6551Account
19 * @notice The token bound account behind every Seat. Accrued tokenized equity lives
20 * here, so it transfers with the NFT on a secondary sale.
21 *
22 * @dev WITHDRAWALS ARE ZERO FEE, FOREVER. There is no fee variable, no owner, and no
23 * admin on this contract — only the current holder of the Seat can move assets,
24 * and nothing is skimmed on the way out.
25 *
26 * Deployed as an ERC-1167 proxy whose runtime footer holds
27 * `abi.encode(salt, chainId, tokenContract, tokenId)`.
28 */
29contract K401Seat6551Account is IERC165, IERC1271 {
30 using SafeERC20 for IERC20;
31 
32 uint256 public state;
33 
34 // Manual reentrancy guard: this contract is used behind a constructor-less proxy,
35 // so it must be safe when its storage starts at zero.
36 uint256 private _locked;
37 
38 event AccountExecuted(address indexed to, uint256 value, bytes data);
39 event Withdrawn(address indexed token, address indexed to, uint256 amount);
40 
41 error NotAccountOwner();
42 error UnsupportedOperation();
43 error CallFailed();
44 error Reentrancy();
45 
46 receive() external payable {}
47 
48 modifier nonReentrant() {
49 if (_locked == 1) revert Reentrancy();
50 _locked = 1;
51 _;
52 _locked = 0;
53 }
54 
55 /// @notice (chainId, tokenContract, tokenId) read from this proxy's own runtime footer.
56 function token() public view returns (uint256 chainId, address tokenContract, uint256 tokenId) {
57 bytes memory footer = new bytes(0x60);
58 assembly {
59 extcodecopy(address(), add(footer, 0x20), 0x4d, 0x60)
60 }
61 return abi.decode(footer, (uint256, address, uint256));
62 }
63 
64 function owner() public view returns (address) {
65 (uint256 chainId, address tokenContract, uint256 tokenId) = token();
66 if (chainId != block.chainid) return address(0);
67 return IERC721(tokenContract).ownerOf(tokenId);
68 }
69 
70 function isValidSigner(address signer, bytes calldata) external view returns (bytes4) {
71 if (signer == owner()) return IERC6551Account.isValidSigner.selector;
72 return bytes4(0);
73 }
74 
75 function isValidSignature(bytes32 hash, bytes memory signature) external view override returns (bytes4) {
76 if (SignatureChecker.isValidSignatureNow(owner(), hash, signature)) {
77 return IERC1271.isValidSignature.selector;
78 }
79 return bytes4(0);
80 }
81 
82 /// @notice Arbitrary call from the account. Owner only. CALL only (operation 0).
83 function execute(address to, uint256 value, bytes calldata data, uint8 operation)
84 external
85 payable
86 nonReentrant
87 returns (bytes memory result)
88 {
89 if (msg.sender != owner()) revert NotAccountOwner();
90 if (operation != 0) revert UnsupportedOperation();
91 unchecked {
92 ++state;
93 }
94 bool ok;
95 (ok, result) = to.call{value: value}(data);
96 if (!ok) {
97 assembly {
98 revert(add(result, 32), mload(result))
99 }
100 }
101 emit AccountExecuted(to, value, data);
102 }
103 
104 /// @notice Zero-fee ERC20 withdrawal. Checks-effects-interactions + reentrancy guard.
105 function withdrawToken(address tokenAddr, address to, uint256 amount) external nonReentrant {
106 if (msg.sender != owner()) revert NotAccountOwner();
107 unchecked {
108 ++state;
109 }
110 IERC20(tokenAddr).safeTransfer(to, amount);
111 emit Withdrawn(tokenAddr, to, amount);
112 }
113 
114 /// @notice Zero-fee native withdrawal.
115 function withdrawETH(address payable to, uint256 amount) external nonReentrant {
116 if (msg.sender != owner()) revert NotAccountOwner();
117 unchecked {
118 ++state;
119 }
120 (bool ok,) = to.call{value: amount}("");
121 if (!ok) revert CallFailed();
122 emit Withdrawn(address(0), to, amount);
123 }
124 
125 function onERC721Received(address, address, uint256, bytes calldata) external pure returns (bytes4) {
126 return this.onERC721Received.selector;
127 }
128 
129 function onERC1155Received(address, address, uint256, uint256, bytes calldata) external pure returns (bytes4) {
130 return this.onERC1155Received.selector;
131 }
132 
133 function onERC1155BatchReceived(address, address, uint256[] calldata, uint256[] calldata, bytes calldata)
134 external
135 pure
136 returns (bytes4)
137 {
138 return this.onERC1155BatchReceived.selector;
139 }
140 
141 function supportsInterface(bytes4 interfaceId) external pure override returns (bool) {
142 return interfaceId == type(IERC165).interfaceId || interfaceId == type(IERC1271).interfaceId
143 || interfaceId == type(IERC6551Account).interfaceId;
144 }
145}
146 
147interface IERC6551Account {
148 function token() external view returns (uint256 chainId, address tokenContract, uint256 tokenId);
149 function state() external view returns (uint256);
150 function isValidSigner(address signer, bytes calldata context) external view returns (bytes4);
151}
152 
153/*//////////////////////////////////////////////////////////////
154 MANAGER
155//////////////////////////////////////////////////////////////*/
156 
157/**
158 * @title K401Seat6551
159 * @notice Lazy token-bound-account deployer for Seats.
160 * @dev Accounts are only deployed on the FIRST stock delivery to a Seat. Every Seat has a
161 * deterministic account address from the moment it is minted, so plain NFT transfers
162 * stay cheap and no gas is spent on Seats that never receive equity.
163 */
164contract K401Seat6551 {
165 IERC6551Registry public immutable registry;
166 address public immutable accountImplementation;
167 /// @notice The ERC721 mirror of K401 — the NFT contract the accounts are bound to.
168 address public immutable seatNft;
169 bytes32 public constant SALT = bytes32(0);
170 
171 mapping(uint256 => bool) public isDeployed;
172 
173 event TbaDeployed(uint256 indexed tokenId, address account);
174 
175 constructor(address registry_, address accountImplementation_, address seatNft_) {
176 registry = IERC6551Registry(registry_);
177 accountImplementation = accountImplementation_;
178 seatNft = seatNft_;
179 }
180 
181 /// @notice Deterministic account address for `tokenId`, whether deployed or not.
182 function tbaOf(uint256 tokenId) public view returns (address) {
183 return registry.account(accountImplementation, SALT, block.chainid, seatNft, tokenId);
184 }
185 
186 /// @notice Deploy the account if needed and return it. Idempotent, permissionless.
187 function tbaFor(uint256 tokenId) external returns (address acct) {
188 acct = registry.createAccount(accountImplementation, SALT, block.chainid, seatNft, tokenId);
189 if (!isDeployed[tokenId]) {
190 isDeployed[tokenId] = true;
191 emit TbaDeployed(tokenId, acct);
192 }
193 }
194}
195 

Click any line number to deep-link to it — the target line highlights on load.