-
Notifications
You must be signed in to change notification settings - Fork 0
feat(contracts): support signed revoke and batch buyer accounting migration #24
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
76b4763
a5f3b96
3a2a488
80cefa8
ab92915
0c8655d
7698493
29fcc88
76ef5e4
6794c9f
cf753d4
879cfac
2c7c935
5298893
9e5976c
97d1d9d
15a26f7
e336c54
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,14 +6,17 @@ import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/U | |
|
|
||
| interface IERC20 { | ||
| function transfer(address to, uint256 amount) external returns (bool); | ||
|
|
||
| function transferFrom(address from, address to, uint256 amount) external returns (bool); | ||
|
|
||
| function approve(address spender, uint256 amount) external returns (bool); | ||
|
|
||
| function balanceOf(address account) external view returns (uint256); | ||
| } | ||
|
|
||
| import { IAntseedChannels } from "./interfaces/IAntseedChannels.sol"; | ||
| import { IAntseedDeposits } from "./interfaces/IAntseedDeposits.sol"; | ||
| import { IAntseedRegistry } from "./interfaces/IAntseedRegistry.sol"; | ||
| import {IAntseedChannels} from "./interfaces/IAntseedChannels.sol"; | ||
| import {IAntseedDeposits} from "./interfaces/IAntseedDeposits.sol"; | ||
| import {IAntseedRegistry} from "./interfaces/IAntseedRegistry.sol"; | ||
|
|
||
| contract AntseedBuyerOperator is Initializable, UUPSUpgradeable { | ||
| IAntseedRegistry public immutable registry; | ||
|
|
@@ -25,15 +28,18 @@ contract AntseedBuyerOperator is Initializable, UUPSUpgradeable { | |
| mapping(bytes32 => bool) public usedDepositIds; | ||
| mapping(address => uint256) public totalPrincipalDeposited; | ||
| mapping(address => uint256) public totalBonusDeposited; | ||
| mapping(address => uint256) public totalWithdrawn; | ||
| mapping(address => uint256) public principalRemaining; | ||
| mapping(address => uint256) public bonusRemaining; | ||
| mapping(address => uint256) public totalPrincipalWithdrawn; | ||
| mapping(address => uint256) public totalBonusWithdrawn; | ||
| mapping(address => uint256) public lastAccountedBalance; | ||
| mapping(address => bool) public buyerAccountingMigrated; | ||
|
|
||
| bytes32 public DOMAIN_SEPARATOR; | ||
| bytes32 public constant WITHDRAW_TYPEHASH = | ||
| keccak256("WithdrawPrincipal(address buyer,uint256 amount,address recipient,uint256 timestamp)"); | ||
| bytes32 public constant REQUEST_CLOSE_TYPEHASH = | ||
| keccak256("RequestClose(bytes32 channelId,uint256 timestamp)"); | ||
| bytes32 public constant WITHDRAW_CHANNEL_TYPEHASH = | ||
| keccak256("WithdrawChannel(bytes32 channelId,uint256 timestamp)"); | ||
| bytes32 public constant WITHDRAW_TYPEHASH = keccak256("WithdrawPrincipal(address buyer,uint256 amount,address recipient,uint256 timestamp)"); | ||
| bytes32 public constant REQUEST_CLOSE_TYPEHASH = keccak256("RequestClose(bytes32 channelId,uint256 timestamp)"); | ||
| bytes32 public constant WITHDRAW_CHANNEL_TYPEHASH = keccak256("WithdrawChannel(bytes32 channelId,uint256 timestamp)"); | ||
| bytes32 public constant REVOKE_OPERATOR_TYPEHASH = keccak256("RevokeOperator(address buyer,uint256 timestamp)"); | ||
|
|
||
| uint256[50] private __gap; | ||
|
|
||
|
|
@@ -44,6 +50,9 @@ contract AntseedBuyerOperator is Initializable, UUPSUpgradeable { | |
| event BuyerDepositFundedWithId(address indexed buyer, uint256 principal, uint256 bonus, string id); | ||
| event BuyerDepositWithdrawn(address indexed buyer, address indexed recipient, uint256 amount); | ||
| event BuyerPrincipalWithdrawn(address indexed buyer, address indexed recipient, uint256 amount); | ||
| event BuyerBonusWithdrawn(address indexed buyer, uint256 amount); | ||
| event BuyerAccountingMigrated(address indexed buyer, uint256 principalRemaining, uint256 bonusRemaining, uint256 accountedBalance); | ||
| event BuyerOperatorRevoked(address indexed buyer); | ||
| event BuyerOperatorTransferred(address indexed buyer, address indexed newOperator); | ||
| event ChannelCloseRequested(bytes32 indexed channelId, address indexed buyer, address indexed caller); | ||
| event ChannelWithdrawn(bytes32 indexed channelId, address indexed buyer, address indexed caller); | ||
|
|
@@ -60,6 +69,7 @@ contract AntseedBuyerOperator is Initializable, UUPSUpgradeable { | |
| error InsufficientPrincipal(); | ||
| error InvalidSignature(); | ||
| error ExpiredSignature(); | ||
| error AlreadyMigrated(); | ||
|
|
||
| modifier onlyOwner() { | ||
| if (msg.sender != owner && msg.sender != admin) revert NotOwner(); | ||
|
|
@@ -100,13 +110,15 @@ contract AntseedBuyerOperator is Initializable, UUPSUpgradeable { | |
| address depositsAddress = registry.deposits(); | ||
| _forceApprove(usdc, depositsAddress, type(uint256).max); | ||
|
|
||
| DOMAIN_SEPARATOR = keccak256(abi.encode( | ||
| keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), | ||
| keccak256("AntseedBuyerOperator"), | ||
| keccak256("1"), | ||
| block.chainid, | ||
| address(this) | ||
| )); | ||
| DOMAIN_SEPARATOR = keccak256( | ||
| abi.encode( | ||
| keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), | ||
| keccak256("AntseedBuyerOperator"), | ||
| keccak256("1"), | ||
| block.chainid, | ||
| address(this) | ||
| ) | ||
| ); | ||
| } | ||
|
|
||
| function _authorizeUpgrade(address) internal override onlyAdmin {} | ||
|
|
@@ -129,29 +141,57 @@ contract AntseedBuyerOperator is Initializable, UUPSUpgradeable { | |
| emit BuyerOperatorAccepted(buyer, nonce); | ||
| } | ||
|
|
||
| function _accountForUsage(address buyer) internal { | ||
| (uint256 available, uint256 reserved, ) = _deposits().getBuyerBalance(buyer); | ||
| uint256 currentBalance = available + reserved; | ||
| uint256 previousBalance = lastAccountedBalance[buyer]; | ||
|
|
||
| if (previousBalance > currentBalance) { | ||
| uint256 used = previousBalance - currentBalance; | ||
| uint256 principalUsed = _min(used, principalRemaining[buyer]); | ||
| principalRemaining[buyer] -= principalUsed; | ||
| used -= principalUsed; | ||
|
|
||
| if (used > 0) { | ||
| bonusRemaining[buyer] -= _min(used, bonusRemaining[buyer]); | ||
| } | ||
| } | ||
|
|
||
| lastAccountedBalance[buyer] = currentBalance; | ||
| } | ||
|
|
||
| function _recordDeposit(address buyer, uint256 principal, uint256 bonus) internal { | ||
| _accountForUsage(buyer); | ||
| totalPrincipalDeposited[buyer] += principal; | ||
| totalBonusDeposited[buyer] += bonus; | ||
| principalRemaining[buyer] += principal; | ||
| bonusRemaining[buyer] += bonus; | ||
| lastAccountedBalance[buyer] += principal + bonus; | ||
| } | ||
|
|
||
| function depositFor(address buyer, uint256 principal, uint256 bonus) external nonReentrant onlyOwner { | ||
| if (buyer == address(0)) revert InvalidAddress(); | ||
| //revoke bonus if we are not operators | ||
| if (_deposits().getOperator(buyer) != address(this)) bonus = 0; | ||
| uint256 total = principal + bonus; | ||
| if (total == 0) revert InvalidAmount(); | ||
| _requireDepositsOperator(buyer); | ||
|
|
||
| totalPrincipalDeposited[buyer] += principal; | ||
| totalBonusDeposited[buyer] += bonus; | ||
| _recordDeposit(buyer, principal, bonus); | ||
| _deposits().deposit(buyer, total); | ||
| emit BuyerDepositFunded(buyer, principal, bonus); | ||
| } | ||
|
|
||
| function depositForWithId(address buyer, uint256 principal, uint256 bonus, string calldata id) external nonReentrant onlyOwner { | ||
| if (buyer == address(0)) revert InvalidAddress(); | ||
| //revoke bonus if we are not operators | ||
| if (_deposits().getOperator(buyer) != address(this)) bonus = 0; | ||
| uint256 total = principal + bonus; | ||
| if (total == 0) revert InvalidAmount(); | ||
| bytes32 idHash = keccak256(bytes(id)); | ||
| if (usedDepositIds[idHash]) revert DuplicateDepositId(); | ||
| usedDepositIds[idHash] = true; | ||
| _requireDepositsOperator(buyer); | ||
|
|
||
| totalPrincipalDeposited[buyer] += principal; | ||
| totalBonusDeposited[buyer] += bonus; | ||
| _recordDeposit(buyer, principal, bonus); | ||
| _deposits().deposit(buyer, total); | ||
| emit BuyerDepositFundedWithId(buyer, principal, bonus, id); | ||
| } | ||
|
|
@@ -172,34 +212,130 @@ contract AntseedBuyerOperator is Initializable, UUPSUpgradeable { | |
| if (signer != buyer) revert InvalidSignature(); | ||
|
|
||
| _requireDepositsOperator(buyer); | ||
| _accountForUsage(buyer); | ||
|
|
||
| if (amount > withdrawablePrincipal(buyer)) revert InsufficientPrincipal(); | ||
|
|
||
| totalWithdrawn[buyer] += amount; | ||
|
|
||
| uint256 beforeBalance = usdc.balanceOf(address(this)); | ||
| _deposits().withdraw(buyer, amount); | ||
| uint256 received = usdc.balanceOf(address(this)) - beforeBalance; | ||
| if (received < amount) revert InvalidAmount(); | ||
|
|
||
| principalRemaining[buyer] -= amount; | ||
| totalPrincipalWithdrawn[buyer] += amount; | ||
| lastAccountedBalance[buyer] -= amount; | ||
|
|
||
| _safeTransfer(usdc, recipient, amount); | ||
| emit BuyerPrincipalWithdrawn(buyer, recipient, amount); | ||
| } | ||
|
|
||
| /// @notice Returns the amount of principal a buyer can still withdraw. | ||
| function withdrawablePrincipal(address buyer) public view returns (uint256) { | ||
| return totalPrincipalDeposited[buyer] > totalWithdrawn[buyer] | ||
| ? totalPrincipalDeposited[buyer] - totalWithdrawn[buyer] | ||
| : 0; | ||
| (uint256 available, uint256 reserved, ) = _deposits().getBuyerBalance(buyer); | ||
| uint256 remaining = principalRemaining[buyer]; | ||
| uint256 currentBalance = available + reserved; | ||
| uint256 previousBalance = lastAccountedBalance[buyer]; | ||
|
|
||
| if (previousBalance > currentBalance) { | ||
| uint256 used = previousBalance - currentBalance; | ||
| remaining = used > remaining ? 0 : remaining - used; | ||
| } | ||
|
|
||
| return _min(remaining, available); | ||
| } | ||
|
|
||
| function _withdrawUnusedBonus(address buyer) internal returns (uint256 amount) { | ||
| _accountForUsage(buyer); | ||
| (uint256 available, , ) = _deposits().getBuyerBalance(buyer); | ||
| if (available <= principalRemaining[buyer]) return 0; | ||
|
|
||
| amount = _min(available - principalRemaining[buyer], bonusRemaining[buyer]); | ||
| if (amount == 0) return 0; | ||
|
|
||
| _deposits().withdraw(buyer, amount); | ||
| bonusRemaining[buyer] -= amount; | ||
| totalBonusWithdrawn[buyer] += amount; | ||
| lastAccountedBalance[buyer] -= amount; | ||
|
Copilot marked this conversation as resolved.
|
||
|
|
||
| emit BuyerBonusWithdrawn(buyer, amount); | ||
| } | ||
|
|
||
| function _revokeOperator(address buyer) internal { | ||
| _requireDepositsOperator(buyer); | ||
| _withdrawUnusedBonus(buyer); | ||
| _deposits().transferOperator(buyer, address(0)); | ||
| emit BuyerOperatorRevoked(buyer); | ||
| } | ||
|
|
||
| function revokeOperator(address buyer, uint256 timestamp, bytes memory buyerSig) public nonReentrant { | ||
| if (buyerSig.length > 0) { | ||
| if (timestamp > block.timestamp || block.timestamp - timestamp > 5 minutes) revert ExpiredSignature(); | ||
| bytes32 structHash = keccak256(abi.encode(REVOKE_OPERATOR_TYPEHASH, buyer, timestamp)); | ||
| bytes32 digest = keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR, structHash)); | ||
| if (_recoverSigner(digest, buyerSig) != buyer) revert InvalidSignature(); | ||
| } else { | ||
| _requireBuyerOrOwner(buyer); | ||
| } | ||
| _revokeOperator(buyer); | ||
| } | ||
|
|
||
| function revokeOperator(address buyer) external { | ||
| revokeOperator(buyer, 0, ""); | ||
| } | ||
|
|
||
| function _migrateBuyerAccounting(address buyer) internal { | ||
| if (buyer == address(0)) revert InvalidAddress(); | ||
| if (buyerAccountingMigrated[buyer]) revert AlreadyMigrated(); | ||
|
|
||
| (uint256 available, uint256 reserved, ) = _deposits().getBuyerBalance(buyer); | ||
| uint256 currentBalance = available + reserved; | ||
|
|
||
| uint256 principal = totalPrincipalDeposited[buyer]; | ||
| uint256 bonus = totalBonusDeposited[buyer]; | ||
| uint256 totalTracked = principal + bonus; | ||
|
|
||
| if (totalTracked > currentBalance) { | ||
| uint256 used = totalTracked - currentBalance; | ||
| uint256 principalUsed = _min(used, principal); | ||
| principal -= principalUsed; | ||
| used -= principalUsed; | ||
|
|
||
| if (used > 0) { | ||
| bonus -= _min(used, bonus); | ||
| } | ||
| } | ||
|
|
||
| principalRemaining[buyer] = principal; | ||
| bonusRemaining[buyer] = bonus; | ||
| lastAccountedBalance[buyer] = currentBalance; | ||
| buyerAccountingMigrated[buyer] = true; | ||
|
|
||
| emit BuyerAccountingMigrated(buyer, principal, bonus, currentBalance); | ||
| } | ||
|
|
||
| /// @notice One-time migration helper for a single buyer funded before remaining-balance accounting existed. | ||
| /// @dev Computes remaining principal/bonus from tracked totals and current deposit balance. | ||
| function migrateBuyerAccounting(address buyer) external onlyOwner { | ||
| _migrateBuyerAccounting(buyer); | ||
| } | ||
|
|
||
| /// @notice One-time migration helper for multiple buyers funded before remaining-balance accounting existed. | ||
| /// @dev Reverts if any buyer is invalid or already migrated. | ||
| function migrateBuyerAccounting(address[] calldata buyers) external onlyOwner { | ||
| uint256 len = buyers.length; | ||
| for (uint256 i = 0; i < len; i++) { | ||
| _migrateBuyerAccounting(buyers[i]); | ||
| } | ||
| } | ||
|
|
||
| function transferBuyerOperator(address buyer, address newOperator) external nonReentrant onlyOwner { | ||
| if (buyer == address(0)) revert InvalidAddress(); | ||
| _withdrawUnusedBonus(buyer); | ||
| _deposits().transferOperator(buyer, newOperator); | ||
| emit BuyerOperatorTransferred(buyer, newOperator); | ||
| } | ||
|
Comment on lines
+350
to
355
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. _revokeOperator added CloseChannelsBeforeRevoke to stop bonus being stranded in reserved channels, but the transfer path has no equivalent check - the operator role can be handed off with bonus still locked.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. thats expected. added a description to the method |
||
|
|
||
| function requestClose(bytes32 channelId, uint256 timestamp, bytes calldata buyerSig) public nonReentrant { | ||
| function requestClose(bytes32 channelId, uint256 timestamp, bytes memory buyerSig) public nonReentrant { | ||
| address buyer = _channelBuyer(channelId); | ||
| if (buyerSig.length > 0) { | ||
| if (timestamp > block.timestamp || block.timestamp - timestamp > 5 minutes) revert ExpiredSignature(); | ||
|
|
@@ -213,14 +349,11 @@ contract AntseedBuyerOperator is Initializable, UUPSUpgradeable { | |
| emit ChannelCloseRequested(channelId, buyer, msg.sender); | ||
| } | ||
|
|
||
| function requestClose(bytes32 channelId) external nonReentrant { | ||
| address buyer = _channelBuyer(channelId); | ||
| _requireBuyerOrOwner(buyer); | ||
| _channels().requestClose(channelId); | ||
| emit ChannelCloseRequested(channelId, buyer, msg.sender); | ||
| function requestClose(bytes32 channelId) external { | ||
| requestClose(channelId, 0, ""); | ||
| } | ||
|
|
||
| function withdrawChannel(bytes32 channelId, uint256 timestamp, bytes calldata buyerSig) external nonReentrant { | ||
| function withdrawChannel(bytes32 channelId, uint256 timestamp, bytes memory buyerSig) public nonReentrant { | ||
| address buyer = _channelBuyer(channelId); | ||
| if (buyerSig.length > 0) { | ||
| if (timestamp > block.timestamp || block.timestamp - timestamp > 5 minutes) revert ExpiredSignature(); | ||
|
|
@@ -234,11 +367,8 @@ contract AntseedBuyerOperator is Initializable, UUPSUpgradeable { | |
| emit ChannelWithdrawn(channelId, buyer, msg.sender); | ||
| } | ||
|
|
||
| function withdrawChannel(bytes32 channelId) external nonReentrant { | ||
| address buyer = _channelBuyer(channelId); | ||
| _requireBuyerOrOwner(buyer); | ||
| _channels().withdraw(channelId); | ||
| emit ChannelWithdrawn(channelId, buyer, msg.sender); | ||
| function withdrawChannel(bytes32 channelId) external { | ||
| withdrawChannel(channelId, 0, ""); | ||
| } | ||
|
|
||
| function sweepToken(address token, address recipient, uint256 amount) external onlyAdmin { | ||
|
|
@@ -262,7 +392,7 @@ contract AntseedBuyerOperator is Initializable, UUPSUpgradeable { | |
| } | ||
|
|
||
| function _channelBuyer(bytes32 channelId) internal view returns (address buyer) { | ||
| (buyer,,,,,,,,) = IAntseedChannelsState(address(_channels())).channels(channelId); | ||
| (buyer, , , , , , , , ) = IAntseedChannelsState(address(_channels())).channels(channelId); | ||
| if (buyer == address(0)) revert InvalidAddress(); | ||
| } | ||
|
|
||
|
|
@@ -293,24 +423,30 @@ contract AntseedBuyerOperator is Initializable, UUPSUpgradeable { | |
| _safeApprove(token, spender, amount); | ||
| } | ||
|
|
||
| function _recoverSigner(bytes32 digest, bytes calldata sig) internal pure returns (address) { | ||
| function _recoverSigner(bytes32 digest, bytes memory sig) internal pure returns (address) { | ||
| if (sig.length != 65) return address(0); | ||
| bytes32 r; | ||
| bytes32 s; | ||
| uint8 v; | ||
| assembly { | ||
| r := calldataload(sig.offset) | ||
| s := calldataload(add(sig.offset, 32)) | ||
| v := byte(0, calldataload(add(sig.offset, 64))) | ||
| r := mload(add(sig, 32)) | ||
| s := mload(add(sig, 64)) | ||
| v := byte(0, mload(add(sig, 96))) | ||
| } | ||
| if (v < 27) v += 27; | ||
| if (v != 27 && v != 28) return address(0); | ||
| return ecrecover(digest, v, r, s); | ||
| } | ||
|
|
||
| function _min(uint256 a, uint256 b) private pure returns (uint256) { | ||
| return a < b ? a : b; | ||
| } | ||
| } | ||
|
|
||
| interface IAntseedChannelsState { | ||
| function channels(bytes32 channelId) | ||
| function channels( | ||
| bytes32 channelId | ||
| ) | ||
| external | ||
| view | ||
| returns ( | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.