Skip to content
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

Exploration on contracts for NotoDepositWithdraw #485

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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
141 changes: 141 additions & 0 deletions solidity/contracts/domains/noto/NotoDepositWithdraw.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.20;

import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import {Noto} from "./Noto.sol";
import {NotoERC20, NotoERC20Factory} from "./NotoERC20.sol";

/**
* @dev Noto variant that allows withdrawing Noto into an ephemeral ERC20
* token, then later depositing back as Noto.
*/
contract NotoDepositWithdraw is Noto {
NotoERC20Factory internal _erc20Factory;
mapping(bytes32 => address) internal _withdrawals;

bytes32 private constant WITHDRAW_TYPEHASH =
keccak256("Withdraw(address to,uint256 amount)");
bytes32 private constant DEPOSIT_TYPEHASH =
keccak256("Deposit(address from,uint256 amount)");

error NotoNoInputs();
error NotoNoOutputs();
error NotoInvalidSigner(address expected, address actual);

function initialize(
address notaryAddress,
bytes calldata data
) public virtual override initializer returns (bytes memory) {
_erc20Factory = new NotoERC20Factory();
return super.initialize(notaryAddress, data);
}

/**
* @dev Withdraw some Noto UTXOs into a new ERC20 contract.
*
* @param inputs Array of zero or more outputs of a previous function call against this
* contract that have not yet been spent, and the signer is authorized to spend
* @param outputs Array of zero or more new outputs to generate, for future transactions to spend
* @param withdrawalOutputs Array of one or more new outputs that will track the ERC20 token balance
* @param to Address to receive the new ERC20 tokens
* @param amount Amount of ERC20 tokens to create
* @param data Any additional transaction data (opaque to the blockchain)
*/
function withdraw(
bytes32[] memory inputs,
bytes32[] memory outputs,
bytes32[] memory withdrawalOutputs,
address to,
uint256 amount,
bytes memory transferSignature,
bytes memory withdrawSignature,
bytes memory data
) external onlyNotary {
if (withdrawalOutputs.length == 0) {
revert NotoNoOutputs();
}

_transfer(inputs, outputs, transferSignature, data);

bytes32 hash = _hashTypedDataV4(
keccak256(abi.encode(WITHDRAW_TYPEHASH, to, amount))
);
address signer = ECDSA.recover(hash, withdrawSignature);
if (signer != to) {
revert NotoInvalidSigner(to, signer);
}

address erc20 = _erc20Factory.create("", "", to, amount);
_checkWithdrawalOutputs(withdrawalOutputs, erc20);
}

/**
* @dev Redeem ERC20 tokens from a previous withdrawal, creating new Noto UTXOs.
*
* @param withdrawalInputs Array of zero or more outputs of a previous withdrawal on this
* contract that have not yet been spent, and the signer is authorized to spend
* @param withdrawalOutputs Array of one or more new outputs that will track the remaining
* ERC20 token balance
* @param outputs Array of zero or more new outputs to generate, for future transactions to spend
* @param from Address from which to redeem ERC20 tokens
* @param amount Amount of ERC20 tokens to redeem
* @param data Any additional transaction data (opaque to the blockchain)
*/
function deposit(
bytes32[] memory withdrawalInputs,
bytes32[] memory withdrawalOutputs,
bytes32[] memory outputs,
address from,
uint256 amount,
bytes memory transferSignature,
bytes memory depositSignature,
bytes memory data
) external onlyNotary {
if (withdrawalInputs.length == 0) {
revert NotoNoInputs();
}
address erc20 = _withdrawals[withdrawalInputs[0]];
if (erc20 == address(0)) {
revert NotoInvalidInput(withdrawalInputs[0]);
}

bytes32[] memory inputs;
_transfer(inputs, outputs, transferSignature, data);

bytes32 hash = _hashTypedDataV4(
keccak256(abi.encode(DEPOSIT_TYPEHASH, from, amount))
);
address signer = ECDSA.recover(hash, depositSignature);
if (signer != from) {
revert NotoInvalidSigner(from, signer);
}

_checkWithdrawalInputs(withdrawalInputs, erc20);
_checkWithdrawalOutputs(withdrawalOutputs, erc20);
NotoERC20(erc20).redeem(from, amount);
}

function _checkWithdrawalInputs(
bytes32[] memory inputs,
address erc20
) internal {
for (uint256 i = 0; i < inputs.length; ++i) {
if (_withdrawals[inputs[i]] != erc20) {
revert NotoInvalidInput(inputs[i]);
}
delete _withdrawals[inputs[i]];
}
}

function _checkWithdrawalOutputs(
bytes32[] memory outputs,
address erc20
) internal {
for (uint256 i = 0; i < outputs.length; ++i) {
if (_withdrawals[outputs[i]] != address(0)) {
revert NotoInvalidOutput(outputs[i]);
}
_withdrawals[outputs[i]] = erc20;
}
}
}
122 changes: 122 additions & 0 deletions solidity/contracts/domains/noto/NotoERC20.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// SPDX-License-Identifier: Apache-2.0
// Compatible with OpenZeppelin Contracts ^5.0.0
pragma solidity ^0.8.20;

import {ERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";
import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol";

/**
* @dev Lightweight ERC20, with a fixed balance allocated to a single receiver at creation.
*/
contract NotoERC20 is Initializable, ERC20Upgradeable, OwnableUpgradeable {
mapping(address => uint256) private _pendingDebit;
mapping(address => uint256) private _pendingCredit;

event UnconfirmedTransfer(
address indexed from,
address indexed to,
uint256 value
);

error TransferPending(address account);
error IncorrectAmount(address account, uint256 expected, uint256 actual);

/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}

function initialize(
string memory name,
string memory symbol,
address owner,
address receiver,
uint256 supply
) public initializer {
__ERC20_init(name, symbol);
__Ownable_init(owner);
_mint(receiver, supply);
}

function redeem(address account, uint256 value) public onlyOwner {
_burn(account, value);
}

function _update(
address from,
address to,
uint256 value
) internal virtual override {
if (from == address(0) || to == address(0)) {
super._update(from, to, value);
return;
}

if (_pendingDebit[from] > 0 || _pendingCredit[from] > 0) {
revert TransferPending(from);
}
if (_pendingDebit[to] > 0 || _pendingCredit[to] > 0) {
revert TransferPending(to);
}
uint256 fromBalance = balanceOf(from);
if (fromBalance < value) {
revert ERC20InsufficientBalance(from, fromBalance, value);
}
_pendingDebit[from] = value;
_pendingCredit[to] = value;
emit UnconfirmedTransfer(from, to, value);
}

function confirmTransfer(
address from,
address to,
uint256 value
) public onlyOwner {
if (_pendingDebit[from] != value) {
revert IncorrectAmount(from, _pendingDebit[from], value);
}
if (_pendingCredit[to] != value) {
revert IncorrectAmount(to, _pendingCredit[to], value);
}
_pendingDebit[from] = 0;
_pendingCredit[to] = 0;
super._update(from, to, value);
}

function pendingBalanceOf(
address account
) public view virtual returns (uint256) {
return
balanceOf(account) +
_pendingCredit[account] -
_pendingDebit[account];
}
}

contract NotoERC20Factory is Ownable {
address public immutable logic;

constructor() Ownable(_msgSender()) {
logic = address(new NotoERC20());
}

function create(
string memory name,
string memory symbol,
address receiver,
uint256 supply
) public onlyOwner returns (address) {
address instance = Clones.clone(logic);
NotoERC20(instance).initialize(
name,
symbol,
_msgSender(),
receiver,
supply
);
return instance;
}
}
Loading