diff --git a/src/DelegationToken.sol b/src/DelegationToken.sol index b667414..0998443 100644 --- a/src/DelegationToken.sol +++ b/src/DelegationToken.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later pragma solidity 0.8.28; -import {ERC20} from "protocol-v3/misc/ERC20.sol"; +import {ERC20} from "src/misc/ERC20.sol"; import {IDelegationToken, Delegation, Signature} from "src/interfaces/IDelegationToken.sol"; /// @title Delegation Token diff --git a/src/misc/Auth.sol b/src/misc/Auth.sol new file mode 100644 index 0000000..414f485 --- /dev/null +++ b/src/misc/Auth.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.28; + +import {IAuth} from "src/misc/interfaces/IAuth.sol"; + +/// @title Auth +/// @notice Simple authentication pattern +/// @author Based on code from https://github.com/makerdao/dss +abstract contract Auth is IAuth { + /// @inheritdoc IAuth + mapping(address => uint256) public wards; + + constructor(address initialWard) { + wards[initialWard] = 1; + emit Rely(initialWard); + } + + /// @dev Check if the msg.sender has permissions + modifier auth() { + require(wards[msg.sender] == 1, NotAuthorized()); + _; + } + + /// @inheritdoc IAuth + function rely(address user) public auth { + wards[user] = 1; + emit Rely(user); + } + + /// @inheritdoc IAuth + function deny(address user) public auth { + wards[user] = 0; + emit Deny(user); + } +} diff --git a/src/misc/ERC20.sol b/src/misc/ERC20.sol new file mode 100644 index 0000000..02e521b --- /dev/null +++ b/src/misc/ERC20.sol @@ -0,0 +1,204 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.28; + +import {Auth} from "src/misc/Auth.sol"; +import {EIP712Lib} from "src/misc/libraries/EIP712Lib.sol"; +import {SignatureLib} from "src/misc/libraries/SignatureLib.sol"; + +import {IERC20, IERC20Metadata, IERC20Permit} from "src/misc/interfaces/IERC20.sol"; + +/// @title ERC20 +/// @notice Standard ERC-20 implementation, with mint/burn functionality and permit logic. +/// @author Modified from https://github.com/makerdao/xdomain-dss/blob/master/src/Dai.sol +contract ERC20 is Auth, IERC20Metadata, IERC20Permit { + error FileUnrecognizedWhat(); + + /// @inheritdoc IERC20Metadata + string public name; + /// @inheritdoc IERC20Metadata + string public symbol; + /// @inheritdoc IERC20Metadata + uint8 public immutable decimals; + /// @inheritdoc IERC20 + uint256 public totalSupply; + + mapping(address => uint256) private balances; + + /// @inheritdoc IERC20 + mapping(address => mapping(address => uint256)) public allowance; + /// @inheritdoc IERC20Permit + mapping(address => uint256) public nonces; + + // --- EIP712 --- + bytes32 private immutable nameHash; + bytes32 private immutable versionHash; + uint256 public immutable deploymentChainId; + bytes32 private immutable _DOMAIN_SEPARATOR; + bytes32 public constant PERMIT_TYPEHASH = + keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); + + // --- Events --- + event File(bytes32 indexed what, string data); + + constructor(uint8 decimals_) Auth(msg.sender) { + decimals = decimals_; + + nameHash = keccak256(bytes("Centrifuge")); + versionHash = keccak256(bytes("1")); + deploymentChainId = block.chainid; + _DOMAIN_SEPARATOR = EIP712Lib.calculateDomainSeparator(nameHash, versionHash); + } + + function _balanceOf(address user) internal view virtual returns (uint256) { + return balances[user]; + } + + /// @inheritdoc IERC20 + function balanceOf(address user) public view virtual returns (uint256) { + return _balanceOf(user); + } + + function _setBalance(address user, uint256 value) internal virtual { + balances[user] = value; + } + + /// @inheritdoc IERC20Permit + function DOMAIN_SEPARATOR() public view returns (bytes32) { + return block.chainid == deploymentChainId + ? _DOMAIN_SEPARATOR + : EIP712Lib.calculateDomainSeparator(nameHash, versionHash); + } + + // --- Administration --- + function file(bytes32 what, string memory data) public virtual auth { + if (what == "name") name = data; + else if (what == "symbol") symbol = data; + else revert FileUnrecognizedWhat(); + emit File(what, data); + } + + // --- ERC20 Mutations --- + /// @inheritdoc IERC20 + function transfer(address to, uint256 value) public virtual returns (bool) { + require(to != address(0) && to != address(this), InvalidAddress()); + uint256 balance = balanceOf(msg.sender); + require(balance >= value, InsufficientBalance()); + + unchecked { + _setBalance(msg.sender, _balanceOf(msg.sender) - value); + // note: we don't need an overflow check here b/c sum of all balances == totalSupply + _setBalance(to, _balanceOf(to) + value); + } + + emit Transfer(msg.sender, to, value); + + return true; + } + + /// @inheritdoc IERC20 + function transferFrom(address from, address to, uint256 value) public virtual returns (bool) { + return _transferFrom(msg.sender, from, to, value); + } + + function _transferFrom(address sender, address from, address to, uint256 value) internal virtual returns (bool) { + require(to != address(0) && to != address(this), InvalidAddress()); + uint256 balance = balanceOf(from); + require(balance >= value, InsufficientBalance()); + + if (from != sender) { + uint256 allowed = allowance[from][sender]; + if (allowed != type(uint256).max) { + require(allowed >= value, InsufficientAllowance()); + unchecked { + allowance[from][sender] = allowed - value; + } + } + } + + unchecked { + _setBalance(from, _balanceOf(from) - value); + // note: we don't need an overflow check here b/c sum of all balances == totalSupply + _setBalance(to, _balanceOf(to) + value); + } + + emit Transfer(from, to, value); + + return true; + } + + /// @inheritdoc IERC20 + function approve(address spender, uint256 value) external returns (bool) { + allowance[msg.sender][spender] = value; + + emit Approval(msg.sender, spender, value); + + return true; + } + + // --- Mint/Burn --- + function mint(address to, uint256 value) public virtual auth { + require(to != address(0) && to != address(this), InvalidAddress()); + unchecked { + // We don't need an overflow check here b/c balances[to] <= totalSupply + // and there is an overflow check below + _setBalance(to, _balanceOf(to) + value); + } + totalSupply = totalSupply + value; + + emit Transfer(address(0), to, value); + } + + function burn(address from, uint256 value) public virtual auth { + uint256 balance = balanceOf(from); + require(balance >= value, InsufficientBalance()); + + if (from != msg.sender) { + uint256 allowed = allowance[from][msg.sender]; + if (allowed != type(uint256).max) { + require(allowed >= value, InsufficientAllowance()); + + unchecked { + allowance[from][msg.sender] = allowed - value; + } + } + } + + unchecked { + // We don't need overflow checks b/c require(balance >= value) and balance <= totalSupply + _setBalance(from, _balanceOf(from) - value); + totalSupply = totalSupply - value; + } + + emit Transfer(from, address(0), value); + } + + // --- Approve by signature --- + function permit(address owner, address spender, uint256 value, uint256 deadline, bytes memory signature) public { + require(block.timestamp <= deadline, PermitExpired()); + + uint256 nonce; + unchecked { + nonce = nonces[owner]++; + } + + bytes32 digest = keccak256( + abi.encodePacked( + "\x19\x01", + DOMAIN_SEPARATOR(), + keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonce, deadline)) + ) + ); + + require(SignatureLib.isValidSignature(owner, digest, signature), InvalidPermit()); + + allowance[owner][spender] = value; + emit Approval(owner, spender, value); + } + + /// @inheritdoc IERC20Permit + function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) + external + { + permit(owner, spender, value, deadline, abi.encodePacked(r, s, v)); + } +} diff --git a/src/misc/interfaces/IERC20.sol b/src/misc/interfaces/IERC20.sol new file mode 100644 index 0000000..bd4f661 --- /dev/null +++ b/src/misc/interfaces/IERC20.sol @@ -0,0 +1,200 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.28; + +/// @title IERC20 +/// @dev Interface of the ERC20 standard as defined in the EIP. +/// @author Modified from OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/IERC20.sol) +interface IERC20 { + error InvalidAddress(); + error InsufficientBalance(); + error InsufficientAllowance(); + + /** + * @dev Emitted when `value` tokens are moved from one account (`from`) to + * another (`to`). + * + * Note that `value` may be zero. + */ + event Transfer(address indexed from, address indexed to, uint256 value); + + /** + * @dev Emitted when the allowance of a `spender` for an `owner` is set by + * a call to {approve}. `value` is the new allowance. + */ + event Approval(address indexed owner, address indexed spender, uint256 value); + + /** + * @dev Returns the value of tokens in existence. + */ + function totalSupply() external view returns (uint256); + + /** + * @dev Returns the value of tokens owned by `account`. + */ + function balanceOf(address account) external view returns (uint256); + + /** + * @dev Moves a `value` amount of tokens from the caller's account to `to`. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transfer(address to, uint256 value) external returns (bool); + + /** + * @dev Returns the remaining number of tokens that `spender` will be + * allowed to spend on behalf of `owner` through {transferFrom}. This is + * zero by default. + * + * This value changes when {approve} or {transferFrom} are called. + */ + function allowance(address owner, address spender) external view returns (uint256); + + /** + * @dev Sets a `value` amount of tokens as the allowance of `spender` over the + * caller's tokens. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * IMPORTANT: Beware that changing an allowance with this method brings the risk + * that someone may use both the old and the new allowance by unfortunate + * transaction ordering. One possible solution to mitigate this race + * condition is to first reduce the spender's allowance to 0 and set the + * desired value afterwards: + * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + * + * Emits an {Approval} event. + */ + function approve(address spender, uint256 value) external returns (bool); + + /** + * @dev Moves a `value` amount of tokens from `from` to `to` using the + * allowance mechanism. `value` is then deducted from the caller's + * allowance. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transferFrom(address from, address to, uint256 value) external returns (bool); +} + +/** + * @dev Interface for the optional metadata functions from the ERC20 standard. + */ +interface IERC20Metadata is IERC20 { + /** + * @dev Returns the name of the token. + */ + function name() external view returns (string memory); + + /** + * @dev Returns the symbol of the token. + */ + function symbol() external view returns (string memory); + + /** + * @dev Returns the decimals places of the token. + */ + function decimals() external view returns (uint8); +} + +/** + * @dev Interface of the ERC20 Permit extension allowing approvals to be made via signatures, as defined in + * https://eips.ethereum.org/EIPS/eip-2612[EIP-2612]. + * + * Adds the {permit} method, which can be used to change an account's ERC20 allowance (see {IERC20-allowance}) by + * presenting a message signed by the account. By not relying on {IERC20-approve}, the token holder account doesn't + * need to send a transaction, and thus is not required to hold Ether at all. + * + * ==== Security Considerations + * + * There are two important considerations concerning the use of `permit`. The first is that a valid permit signature + * expresses an allowance, and it should not be assumed to convey additional meaning. In particular, it should not be + * considered as an intention to spend the allowance in any specific way. The second is that because permits have + * built-in replay protection and can be submitted by anyone, they can be frontrun. A protocol that uses permits should + * take this into consideration and allow a `permit` call to fail. Combining these two aspects, a pattern that may be + * generally recommended is: + * + * ```solidity + * function doThingWithPermit(..., uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) public { + * try token.permit(msg.sender, address(this), value, deadline, v, r, s) {} catch {} + * doThing(..., value); + * } + * + * function doThing(..., uint256 value) public { + * token.safeTransferFrom(msg.sender, address(this), value); + * ... + * } + * ``` + * + * Observe that: 1) `msg.sender` is used as the owner, leaving no ambiguity as to the signer intent, and 2) the use of + * `try/catch` allows the permit to fail and makes the code tolerant to frontrunning. (See also + * {SafeERC20-safeTransferFrom}). + * + * Additionally, note that smart contract wallets (such as Argent or Safe) are not able to produce permit signatures, so + * contracts should have entry points that don't rely on permit. + */ +interface IERC20Permit { + error PermitExpired(); + error InvalidPermit(); + + /** + * @dev Sets `value` as the allowance of `spender` over ``owner``'s tokens, + * given ``owner``'s signed approval. + * + * IMPORTANT: The same issues {IERC20-approve} has related to transaction + * ordering also apply here. + * + * Emits an {Approval} event. + * + * Requirements: + * + * - `spender` cannot be the zero address. + * - `deadline` must be a timestamp in the future. + * - `v`, `r` and `s` must be a valid `secp256k1` signature from `owner` + * over the EIP712-formatted function arguments. + * - the signature must use ``owner``'s current nonce (see {nonces}). + * + * For more information on the signature format, see the + * https://eips.ethereum.org/EIPS/eip-2612#specification[relevant EIP + * section]. + * + * CAUTION: See Security Considerations above. + */ + function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) + external; + + /** + * @dev Returns the current nonce for `owner`. This value must be + * included whenever a signature is generated for {permit}. + * + * Every successful call to {permit} increases ``owner``'s nonce by one. This + * prevents a signature from being used multiple times. + */ + function nonces(address owner) external view returns (uint256); + + /** + * @dev Returns the domain separator used in the encoding of the signature for {permit}, as defined by {EIP712}. + */ + // solhint-disable-next-line func-name-mixedcase + function DOMAIN_SEPARATOR() external view returns (bytes32); +} + +interface IERC20Wrapper { + /** + * @dev Returns the address of the underlying ERC-20 token that is being wrapped. + */ + function underlying() external view returns (address); + + /** + * @dev Allow a user to deposit underlying tokens and mint the corresponding number of wrapped tokens. + */ + function depositFor(address account, uint256 value) external returns (bool); + + /** + * @dev Allow a user to burn a number of wrapped tokens and withdraw the corresponding number of underlying tokens. + */ + function withdrawTo(address account, uint256 value) external returns (bool); +} diff --git a/src/misc/libraries/EIP712Lib.sol b/src/misc/libraries/EIP712Lib.sol new file mode 100644 index 0000000..514b1b1 --- /dev/null +++ b/src/misc/libraries/EIP712Lib.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.28; + +/// @title EIP712 Lib +library EIP712Lib { + // keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)") + bytes32 public constant EIP712_DOMAIN_TYPEHASH = 0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f; + + function calculateDomainSeparator(bytes32 nameHash, bytes32 versionHash) internal view returns (bytes32) { + return keccak256(abi.encode(EIP712_DOMAIN_TYPEHASH, nameHash, versionHash, block.chainid, address(this))); + } +} diff --git a/src/misc/libraries/SignatureLib.sol b/src/misc/libraries/SignatureLib.sol new file mode 100644 index 0000000..dfd46eb --- /dev/null +++ b/src/misc/libraries/SignatureLib.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.28; + +interface IERC1271 { + function isValidSignature(bytes32, bytes memory) external view returns (bytes4); +} + +/// @title Signature Lib +library SignatureLib { + error InvalidSigner(); + + function isValidSignature(address signer, bytes32 digest, bytes memory signature) + internal + view + returns (bool valid) + { + require(signer != address(0), InvalidSigner()); + + 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 (signer == ecrecover(digest, v, r, s)) { + return true; + } + } + + if (signer.code.length > 0) { + (bool success, bytes memory result) = + signer.staticcall(abi.encodeCall(IERC1271.isValidSignature, (digest, signature))); + valid = + (success && result.length == 32 && abi.decode(result, (bytes4)) == IERC1271.isValidSignature.selector); + } + } +} diff --git a/test/CFG.t.sol b/test/CFG.t.sol index c9f846a..1a2a7dd 100644 --- a/test/CFG.t.sol +++ b/test/CFG.t.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.28; import {CFG} from "src/CFG.sol"; import {IDelegationToken, Delegation, Signature} from "src/interfaces/IDelegationToken.sol"; -import {IAuth} from "protocol-v3/misc/interfaces/IAuth.sol"; +import {IAuth} from "src/misc/interfaces/IAuth.sol"; import "forge-std/Test.sol"; contract CFGTest is Test {