mirror of
https://github.com/placeholder-soft/gifted-contracts-v2.git
synced 2026-04-30 11:02:36 +08:00
feat: add token bound account handling
This commit is contained in:
Submodule lib/reference updated: 43a84573bb...43955c3c3c
@@ -2,4 +2,6 @@ ds-test/=lib/openzeppelin-contracts-upgradeable/lib/forge-std/lib/ds-test/src/
|
||||
erc4626-tests/=lib/openzeppelin-contracts-upgradeable/lib/erc4626-tests/
|
||||
forge-std/=lib/forge-std/src/
|
||||
@openzeppelin/=lib/openzeppelin-contracts/contracts/
|
||||
@openzeppelin-contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/
|
||||
@openzeppelin-contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/
|
||||
erc6551/=lib/reference/src
|
||||
openzeppelin-contracts/=lib/openzeppelin-contracts/contracts/
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-License-Identifier: UNLICENSED
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.13;
|
||||
|
||||
import {Script, console} from "forge-std/Script.sol";
|
||||
|
||||
67
src/ERC20Wad.sol
Normal file
67
src/ERC20Wad.sol
Normal file
@@ -0,0 +1,67 @@
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
||||
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
|
||||
import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
|
||||
|
||||
library ERC20Wad {
|
||||
using SafeERC20 for IERC20;
|
||||
using SafeERC20 for IERC20Metadata;
|
||||
using SafeERC20 for ERC20Burnable;
|
||||
|
||||
function wad_safeTransfer(IERC20Metadata _token, address _to, uint256 _amount) internal {
|
||||
if (_amount > 0) {
|
||||
_token.safeTransfer(_to, _amount / (10 ** (18 - _token.decimals())));
|
||||
}
|
||||
}
|
||||
|
||||
function wad_safeTransferFrom(IERC20Metadata _token, address _from, address _to, uint256 _amount) internal {
|
||||
if (_amount > 0) {
|
||||
_token.safeTransferFrom(_from, _to, _amount / (10 ** (18 - _token.decimals())));
|
||||
}
|
||||
}
|
||||
|
||||
function wad_balanceOf(IERC20Metadata _token, address _owner) internal view returns (uint256) {
|
||||
return _token.balanceOf(_owner) * (10 ** (18 - _token.decimals()));
|
||||
}
|
||||
|
||||
function wad_totalSupply(IERC20Metadata _token) internal view returns (uint256) {
|
||||
return _token.totalSupply() * (10 ** (18 - _token.decimals()));
|
||||
}
|
||||
|
||||
function wad_allowance(IERC20Metadata _token, address _owner, address _spender) internal view returns (uint256) {
|
||||
return _token.allowance(_owner, _spender) * (10 ** (18 - _token.decimals()));
|
||||
}
|
||||
|
||||
function wad_forceApprove(IERC20Metadata _token, address _spender, uint256 _amount) internal returns (bool) {
|
||||
_token.forceApprove(_spender, _amount / (10 ** (18 - _token.decimals())));
|
||||
return true;
|
||||
}
|
||||
|
||||
function wad_safeIncreaseAllowance(IERC20Metadata _token, address _spender, uint256 _addedValue)
|
||||
internal
|
||||
returns (bool)
|
||||
{
|
||||
_token.safeIncreaseAllowance(_spender, _addedValue / (10 ** (18 - _token.decimals())));
|
||||
return true;
|
||||
}
|
||||
|
||||
function wad_safeDecreaseAllowance(IERC20Metadata _token, address _spender, uint256 _subtractedValue)
|
||||
internal
|
||||
returns (bool)
|
||||
{
|
||||
_token.safeDecreaseAllowance(_spender, _subtractedValue / (10 ** (18 - _token.decimals())));
|
||||
return true;
|
||||
}
|
||||
|
||||
function wad_burn(ERC20Burnable _token, uint256 _amount) internal {
|
||||
_token.burn(_amount / (10 ** (18 - _token.decimals())));
|
||||
}
|
||||
|
||||
function wad_burnFrom(ERC20Burnable _token, address _from, uint256 _amount) internal {
|
||||
_token.burnFrom(_from, _amount / (10 ** (18 - _token.decimals())));
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import "@openzeppelin/proxy/utils/UUPSUpgradeable.sol";
|
||||
import "@openzeppelin-contracts-upgradeable/proxy/utils/Initializable.sol";
|
||||
import "./interfaces/IGiftedAccountGuardian.sol";
|
||||
import "./interfaces/IGiftedAccount.sol";
|
||||
import "./interfaces/IGiftedBox.sol";
|
||||
|
||||
error UntrustedImplementation();
|
||||
error NotAuthorized();
|
||||
@@ -44,9 +45,15 @@ contract GiftedAccount is
|
||||
function initialize(address guardian) public initializer {
|
||||
_guardian = IGiftedAccountGuardian(guardian);
|
||||
}
|
||||
|
||||
/// events
|
||||
|
||||
event CallPermit(address indexed owner, address indexed to, uint256 nonce, uint256 deadline);
|
||||
event CallPermit(
|
||||
address indexed owner,
|
||||
address indexed to,
|
||||
uint256 nonce,
|
||||
uint256 deadline
|
||||
);
|
||||
|
||||
// Event to log the transfer of an ERC1155 token with a permit
|
||||
event CallTransferERC1155Permit(
|
||||
@@ -64,9 +71,35 @@ contract GiftedAccount is
|
||||
/// @param sender The address that sent the ETH
|
||||
/// @param amount The amount of ETH sent
|
||||
/// @param newBalance The new balance of the account
|
||||
event ReceivedEther(address indexed sender, uint256 amount, uint256 newBalance);
|
||||
event ReceivedEther(
|
||||
address indexed sender,
|
||||
uint256 amount,
|
||||
uint256 newBalance
|
||||
);
|
||||
|
||||
event AccountGuardianUpgraded(address indexed previousGuardian, address indexed newGuardian);
|
||||
event AccountGuardianUpgraded(
|
||||
address indexed previousGuardian,
|
||||
address indexed newGuardian
|
||||
);
|
||||
|
||||
event GiftedAccountERC1155Received(
|
||||
address operator,
|
||||
address from,
|
||||
uint256 erc1155TokenId,
|
||||
uint256 erc1155Tokenvalue,
|
||||
address erc1155Contract,
|
||||
address giftedBoxContract,
|
||||
uint256 giftedBoxTokenId
|
||||
);
|
||||
|
||||
event GiftedAccountERC721Received(
|
||||
address operator,
|
||||
address from,
|
||||
uint256 erc721TokenId,
|
||||
address erc721Contract,
|
||||
address giftedBoxContract,
|
||||
uint256 giftedBoxTokenId
|
||||
);
|
||||
/// modifier
|
||||
|
||||
/// @dev reverts if caller is not authorized to execute on this account
|
||||
@@ -85,12 +118,11 @@ contract GiftedAccount is
|
||||
emit ReceivedEther(msg.sender, msg.value, address(this).balance);
|
||||
}
|
||||
|
||||
function executeCall(address to, uint256 value, bytes calldata data)
|
||||
external
|
||||
payable
|
||||
onlyAuthorized
|
||||
returns (bytes memory result)
|
||||
{
|
||||
function executeCall(
|
||||
address to,
|
||||
uint256 value,
|
||||
bytes calldata data
|
||||
) external payable onlyAuthorized returns (bytes memory result) {
|
||||
return call(to, value, data);
|
||||
}
|
||||
|
||||
@@ -103,23 +135,31 @@ contract GiftedAccount is
|
||||
}
|
||||
|
||||
function owner() public view returns (address) {
|
||||
(uint256 chainId, address tokenContract, uint256 tokenId) = this.token();
|
||||
if (chainId != block.chainid) revert("!chainid-not-equal-block-chainid");
|
||||
(uint256 chainId, address tokenContract, uint256 tokenId) = this
|
||||
.token();
|
||||
if (chainId != block.chainid)
|
||||
revert("!chainid-not-equal-block-chainid");
|
||||
|
||||
return IERC721(tokenContract).ownerOf(tokenId);
|
||||
}
|
||||
|
||||
// ======== ERC165 Interface ========
|
||||
function supportsInterface(bytes4 interfaceId) public pure returns (bool) {
|
||||
return (
|
||||
interfaceId == type(IERC165).interfaceId || interfaceId == type(IERC1155Receiver).interfaceId
|
||||
|| interfaceId == type(IERC6551Account).interfaceId
|
||||
);
|
||||
return (interfaceId == type(IERC165).interfaceId ||
|
||||
interfaceId == type(IERC1155Receiver).interfaceId ||
|
||||
interfaceId == type(IERC6551Account).interfaceId);
|
||||
}
|
||||
|
||||
// ======== ERC1271 Interface ========
|
||||
function isValidSignature(bytes32 hash, bytes memory signature) external view returns (bytes4 magicValue) {
|
||||
bool isValid = SignatureChecker.isValidSignatureNow(owner(), hash, signature);
|
||||
function isValidSignature(
|
||||
bytes32 hash,
|
||||
bytes memory signature
|
||||
) external view returns (bytes4 magicValue) {
|
||||
bool isValid = SignatureChecker.isValidSignatureNow(
|
||||
owner(),
|
||||
hash,
|
||||
signature
|
||||
);
|
||||
|
||||
if (isValid) {
|
||||
return IERC1271.isValidSignature.selector;
|
||||
@@ -128,28 +168,117 @@ contract GiftedAccount is
|
||||
}
|
||||
|
||||
// ============ ERC721Receiver Interface ============
|
||||
function onERC721Received(address, address, uint256, bytes calldata) external pure override returns (bytes4) {
|
||||
return IERC721Receiver.onERC721Received.selector;
|
||||
function onERC721Received(
|
||||
address _operator,
|
||||
address _from,
|
||||
uint256 _tokenId,
|
||||
bytes calldata
|
||||
) external override returns (bytes4) {
|
||||
address _owner = owner();
|
||||
(uint256 chainId, address tokenContract, uint256 tokenId) = this
|
||||
.token();
|
||||
if (chainId != block.chainid)
|
||||
revert("!chainid-not-equal-block-chainid");
|
||||
|
||||
if (
|
||||
_owner == _from ||
|
||||
_owner == _operator ||
|
||||
isAuthorizedSender(_from, _operator, tokenContract, tokenId)
|
||||
) {
|
||||
emit GiftedAccountERC721Received(
|
||||
_operator,
|
||||
_from,
|
||||
_tokenId,
|
||||
msg.sender,
|
||||
tokenContract,
|
||||
tokenId
|
||||
);
|
||||
return IERC721Receiver.onERC721Received.selector;
|
||||
}
|
||||
|
||||
revert("!sender-not-authorized");
|
||||
}
|
||||
|
||||
function isAuthorizedSender(
|
||||
address _from,
|
||||
address _operator,
|
||||
address tokenContract,
|
||||
uint256 tokenId
|
||||
) internal view returns (bool) {
|
||||
GiftingRecord memory record = IGiftedBox(tokenContract)
|
||||
.getGiftingRecord(tokenId);
|
||||
return (record.sender == _from || record.sender == _operator);
|
||||
}
|
||||
|
||||
// ============ ERC1155Receiver Interface ============
|
||||
function onERC1155Received(address, address, uint256, uint256, bytes calldata)
|
||||
external
|
||||
pure
|
||||
override
|
||||
returns (bytes4)
|
||||
{
|
||||
return IERC1155Receiver.onERC1155Received.selector;
|
||||
function onERC1155Received(
|
||||
address _operator,
|
||||
address _from,
|
||||
uint256 _id,
|
||||
uint256 _value,
|
||||
bytes calldata
|
||||
) external override returns (bytes4) {
|
||||
address _owner = owner();
|
||||
(uint256 chainId, address tokenContract, uint256 tokenId) = this
|
||||
.token();
|
||||
if (chainId != block.chainid)
|
||||
revert("!chainid-not-equal-block-chainid");
|
||||
|
||||
if (
|
||||
_owner == _from ||
|
||||
_owner == _operator ||
|
||||
isAuthorizedSender(_from, _operator, tokenContract, tokenId)
|
||||
) {
|
||||
emit GiftedAccountERC1155Received(
|
||||
_operator,
|
||||
_from,
|
||||
_id,
|
||||
_value,
|
||||
msg.sender,
|
||||
tokenContract,
|
||||
tokenId
|
||||
);
|
||||
return IERC1155Receiver.onERC1155Received.selector;
|
||||
}
|
||||
|
||||
revert("!sender-not-authorized");
|
||||
}
|
||||
|
||||
function onERC1155BatchReceived(address, address, uint256[] calldata, uint256[] calldata, bytes calldata)
|
||||
external
|
||||
pure
|
||||
override
|
||||
returns (bytes4)
|
||||
{
|
||||
return IERC1155Receiver.onERC1155BatchReceived.selector;
|
||||
function onERC1155BatchReceived(
|
||||
address _operator,
|
||||
address _from,
|
||||
uint256[] calldata _ids,
|
||||
uint256[] calldata _values,
|
||||
bytes calldata
|
||||
) external override returns (bytes4) {
|
||||
address _owner = owner();
|
||||
(uint256 chainId, address tokenContract, uint256 tokenId) = this
|
||||
.token();
|
||||
if (chainId != block.chainid)
|
||||
revert("!chainid-not-equal-block-chainid");
|
||||
|
||||
if (
|
||||
_owner == _from ||
|
||||
_owner == _operator ||
|
||||
isAuthorizedSender(_from, _operator, tokenContract, tokenId)
|
||||
) {
|
||||
for (uint256 i = 0; i < _ids.length; i++) {
|
||||
emit GiftedAccountERC1155Received(
|
||||
_operator,
|
||||
_from,
|
||||
_ids[i],
|
||||
_values[i],
|
||||
msg.sender,
|
||||
tokenContract,
|
||||
tokenId
|
||||
);
|
||||
}
|
||||
return IERC1155Receiver.onERC1155BatchReceived.selector;
|
||||
}
|
||||
|
||||
revert("!sender-not-authorized");
|
||||
}
|
||||
|
||||
/// EIP 712
|
||||
/// domain separator
|
||||
|
||||
@@ -159,15 +288,18 @@ contract GiftedAccount is
|
||||
|
||||
// Returns the domain separator, updating it if chainID changes
|
||||
function domainSeparator() public view returns (bytes32) {
|
||||
return keccak256(
|
||||
abi.encode(
|
||||
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
|
||||
keccak256(bytes(name())),
|
||||
keccak256(bytes("1")),
|
||||
block.chainid,
|
||||
address(this)
|
||||
)
|
||||
);
|
||||
return
|
||||
keccak256(
|
||||
abi.encode(
|
||||
keccak256(
|
||||
"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
|
||||
),
|
||||
keccak256(bytes(name())),
|
||||
keccak256(bytes("1")),
|
||||
block.chainid,
|
||||
address(this)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/// internal
|
||||
@@ -175,7 +307,8 @@ contract GiftedAccount is
|
||||
function isAuthorized(address caller) public view returns (bool) {
|
||||
if (caller == owner()) return true;
|
||||
|
||||
if (address(_guardian) != address(0) && _guardian.isExecutor(caller)) return true;
|
||||
if (address(_guardian) != address(0) && _guardian.isExecutor(caller))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -199,7 +332,11 @@ contract GiftedAccount is
|
||||
_nonce++;
|
||||
}
|
||||
|
||||
function call(address to, uint256 value, bytes calldata data) internal returns (bytes memory result) {
|
||||
function call(
|
||||
address to,
|
||||
uint256 value,
|
||||
bytes calldata data
|
||||
) internal returns (bytes memory result) {
|
||||
_incrementNonce();
|
||||
|
||||
emit TransactionExecuted(to, value, data);
|
||||
@@ -214,7 +351,12 @@ contract GiftedAccount is
|
||||
}
|
||||
}
|
||||
|
||||
function _recover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) internal pure returns (address) {
|
||||
function _recover(
|
||||
bytes32 hash,
|
||||
uint8 v,
|
||||
bytes32 r,
|
||||
bytes32 s
|
||||
) internal pure returns (address) {
|
||||
// EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature
|
||||
// unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines
|
||||
// the valid range for s in (281): 0 < s < secp256k1n ÷ 2 + 1, and for v in (282): v ∈ {27, 28}. Most
|
||||
@@ -224,7 +366,10 @@ contract GiftedAccount is
|
||||
// with 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - s1 and flip v from 27 to 28 or
|
||||
// vice versa. If your library also generates signatures with 0/1 for v instead 27/28, add 27 to v to accept
|
||||
// these malleable signatures as well.
|
||||
if (uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) {
|
||||
if (
|
||||
uint256(s) >
|
||||
0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0
|
||||
) {
|
||||
revert("ECDSA: invalid signature 's' value");
|
||||
}
|
||||
|
||||
@@ -241,18 +386,25 @@ contract GiftedAccount is
|
||||
|
||||
// d83869c5bb54ba35eb2fa505a0206fde32206a3325ac92b027126dca04d8cdae
|
||||
bytes32 public constant CALL_PERMIT_TYPEHASH =
|
||||
keccak256("CallPermit(address to, uint256 value, byte data, uint256 deadline, uint256 nonce)");
|
||||
keccak256(
|
||||
"CallPermit(address to, uint256 value, byte data, uint256 deadline, uint256 nonce)"
|
||||
);
|
||||
|
||||
/// external
|
||||
|
||||
function getTypedCallPermitHash(address to, uint256 value, bytes calldata data, uint256 deadline)
|
||||
public
|
||||
view
|
||||
returns (bytes32 callHash)
|
||||
{
|
||||
bytes32 hashStruct = keccak256(abi.encode(CALL_PERMIT_TYPEHASH, to, value, data, deadline, nonce()));
|
||||
function getTypedCallPermitHash(
|
||||
address to,
|
||||
uint256 value,
|
||||
bytes calldata data,
|
||||
uint256 deadline
|
||||
) public view returns (bytes32 callHash) {
|
||||
bytes32 hashStruct = keccak256(
|
||||
abi.encode(CALL_PERMIT_TYPEHASH, to, value, data, deadline, nonce())
|
||||
);
|
||||
bytes32 eip712DomainHash = domainSeparator();
|
||||
callHash = keccak256(abi.encodePacked(uint16(0x1901), eip712DomainHash, hashStruct));
|
||||
callHash = keccak256(
|
||||
abi.encodePacked(uint16(0x1901), eip712DomainHash, hashStruct)
|
||||
);
|
||||
}
|
||||
|
||||
function getTypedCallPermitHash(
|
||||
@@ -262,9 +414,20 @@ contract GiftedAccount is
|
||||
uint256 deadline,
|
||||
uint256 encodeNonce
|
||||
) public view returns (bytes32 callHash) {
|
||||
bytes32 hashStruct = keccak256(abi.encode(CALL_PERMIT_TYPEHASH, to, value, data, deadline, encodeNonce));
|
||||
bytes32 hashStruct = keccak256(
|
||||
abi.encode(
|
||||
CALL_PERMIT_TYPEHASH,
|
||||
to,
|
||||
value,
|
||||
data,
|
||||
deadline,
|
||||
encodeNonce
|
||||
)
|
||||
);
|
||||
bytes32 eip712DomainHash = domainSeparator();
|
||||
callHash = keccak256(abi.encodePacked(uint16(0x1901), eip712DomainHash, hashStruct));
|
||||
callHash = keccak256(
|
||||
abi.encodePacked(uint16(0x1901), eip712DomainHash, hashStruct)
|
||||
);
|
||||
}
|
||||
|
||||
function executeTypedCallPermit(
|
||||
@@ -307,43 +470,59 @@ contract GiftedAccount is
|
||||
bytes32 s
|
||||
) external {
|
||||
require(block.timestamp <= deadline, "!call-permit-expired");
|
||||
string memory message = getTransferNFTPermitMessage(tokenContract, tokenId, to, deadline);
|
||||
string memory message = getTransferNFTPermitMessage(
|
||||
tokenContract,
|
||||
tokenId,
|
||||
to,
|
||||
deadline
|
||||
);
|
||||
bytes32 signHash = toEthPersonalSignedMessageHash(bytes(message));
|
||||
|
||||
address signer = _recover(signHash, v, r, s);
|
||||
require(signer == owner(), "!transfer-permit-invalid-signature");
|
||||
|
||||
IERC721(tokenContract).safeTransferFrom(address(this), to, tokenId);
|
||||
emit CallTransferNFTPermit(address(this), to, tokenContract, tokenId, deadline, nonce(), signer, msg.sender);
|
||||
emit CallTransferNFTPermit(
|
||||
address(this),
|
||||
to,
|
||||
tokenContract,
|
||||
tokenId,
|
||||
deadline,
|
||||
nonce(),
|
||||
signer,
|
||||
msg.sender
|
||||
);
|
||||
}
|
||||
|
||||
function getTransferNFTPermitMessage(address tokenContract, uint256 tokenId, address to, uint256 deadline)
|
||||
public
|
||||
view
|
||||
returns (string memory)
|
||||
{
|
||||
return string.concat(
|
||||
"I want to transfer NFT",
|
||||
"\n From: ",
|
||||
address(this).toHexString(),
|
||||
"\n NFT: ",
|
||||
tokenContract.toHexString(),
|
||||
"\n TokenId: ",
|
||||
tokenId.toString(),
|
||||
"\n To: ",
|
||||
to.toHexString(),
|
||||
"\n Before: ",
|
||||
deadline.toString(),
|
||||
".",
|
||||
"\n Nonce: ",
|
||||
nonce().toString(),
|
||||
"\n Chain ID: ",
|
||||
block.chainid.toString(),
|
||||
"\n BY: ",
|
||||
name(),
|
||||
"\n Version: ",
|
||||
"0.0.1"
|
||||
);
|
||||
function getTransferNFTPermitMessage(
|
||||
address tokenContract,
|
||||
uint256 tokenId,
|
||||
address to,
|
||||
uint256 deadline
|
||||
) public view returns (string memory) {
|
||||
return
|
||||
string.concat(
|
||||
"I want to transfer NFT",
|
||||
"\n From: ",
|
||||
address(this).toHexString(),
|
||||
"\n NFT: ",
|
||||
tokenContract.toHexString(),
|
||||
"\n TokenId: ",
|
||||
tokenId.toString(),
|
||||
"\n To: ",
|
||||
to.toHexString(),
|
||||
"\n Before: ",
|
||||
deadline.toString(),
|
||||
".",
|
||||
"\n Nonce: ",
|
||||
nonce().toString(),
|
||||
"\n Chain ID: ",
|
||||
block.chainid.toString(),
|
||||
"\n BY: ",
|
||||
name(),
|
||||
"\n Version: ",
|
||||
"0.0.1"
|
||||
);
|
||||
}
|
||||
|
||||
// Method to transfer ERC1155 tokens with a permit
|
||||
@@ -358,15 +537,35 @@ contract GiftedAccount is
|
||||
bytes32 s
|
||||
) external {
|
||||
require(block.timestamp <= deadline, "!call-permit-expired");
|
||||
string memory message = getTransferERC1155PermitMessage(tokenContract, tokenId, amount, to, deadline);
|
||||
string memory message = getTransferERC1155PermitMessage(
|
||||
tokenContract,
|
||||
tokenId,
|
||||
amount,
|
||||
to,
|
||||
deadline
|
||||
);
|
||||
bytes32 signHash = toEthPersonalSignedMessageHash(bytes(message));
|
||||
|
||||
address signer = ECDSA.recover(signHash, v, r, s);
|
||||
require(signer == owner(), "!transfer-permit-invalid-signature");
|
||||
|
||||
IERC1155(tokenContract).safeTransferFrom(address(this), to, tokenId, amount, "");
|
||||
IERC1155(tokenContract).safeTransferFrom(
|
||||
address(this),
|
||||
to,
|
||||
tokenId,
|
||||
amount,
|
||||
""
|
||||
);
|
||||
emit CallTransferERC1155Permit(
|
||||
address(this), to, tokenContract, tokenId, amount, deadline, nonce(), signer, msg.sender
|
||||
address(this),
|
||||
to,
|
||||
tokenContract,
|
||||
tokenId,
|
||||
amount,
|
||||
deadline,
|
||||
nonce(),
|
||||
signer,
|
||||
msg.sender
|
||||
);
|
||||
}
|
||||
|
||||
@@ -378,30 +577,39 @@ contract GiftedAccount is
|
||||
address to,
|
||||
uint256 deadline
|
||||
) public view returns (string memory) {
|
||||
return string.concat(
|
||||
"I authorize the transfer of ERC1155 tokens",
|
||||
"\n Token Contract: ",
|
||||
Strings.toHexString(uint256(uint160(tokenContract)), 20),
|
||||
"\n Token ID: ",
|
||||
Strings.toString(tokenId),
|
||||
"\n Amount: ",
|
||||
Strings.toString(amount),
|
||||
"\n To: ",
|
||||
Strings.toHexString(uint256(uint160(to)), 20),
|
||||
"\n Deadline: ",
|
||||
Strings.toString(deadline),
|
||||
"\n Nonce: ",
|
||||
nonce().toString(),
|
||||
"\n Chain ID: ",
|
||||
block.chainid.toString(),
|
||||
"\n BY: ",
|
||||
name(),
|
||||
"\n Version: ",
|
||||
"0.0.1"
|
||||
);
|
||||
return
|
||||
string.concat(
|
||||
"I authorize the transfer of ERC1155 tokens",
|
||||
"\n Token Contract: ",
|
||||
Strings.toHexString(uint256(uint160(tokenContract)), 20),
|
||||
"\n Token ID: ",
|
||||
Strings.toString(tokenId),
|
||||
"\n Amount: ",
|
||||
Strings.toString(amount),
|
||||
"\n To: ",
|
||||
Strings.toHexString(uint256(uint160(to)), 20),
|
||||
"\n Deadline: ",
|
||||
Strings.toString(deadline),
|
||||
"\n Nonce: ",
|
||||
nonce().toString(),
|
||||
"\n Chain ID: ",
|
||||
block.chainid.toString(),
|
||||
"\n BY: ",
|
||||
name(),
|
||||
"\n Version: ",
|
||||
"0.0.1"
|
||||
);
|
||||
}
|
||||
|
||||
function toEthPersonalSignedMessageHash(bytes memory _msg) public pure returns (bytes32 signHash) {
|
||||
signHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n", _msg.length.toString(), _msg));
|
||||
function toEthPersonalSignedMessageHash(
|
||||
bytes memory _msg
|
||||
) public pure returns (bytes32 signHash) {
|
||||
signHash = keccak256(
|
||||
abi.encodePacked(
|
||||
"\x19Ethereum Signed Message:\n",
|
||||
_msg.length.toString(),
|
||||
_msg
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-License-Identifier: UNLICENSED
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import "@openzeppelin/access/Ownable2Step.sol";
|
||||
@@ -10,10 +10,6 @@ contract GiftedAccountGuardian is Ownable2Step, IGiftedAccountGuardian {
|
||||
mapping(address => bool) private _isExecutor;
|
||||
address private _implementation;
|
||||
|
||||
mapping(address => address) _customAccountImplementation;
|
||||
|
||||
event CustomAccountImplementationUpdated(address indexed account, address implementation);
|
||||
|
||||
constructor() Ownable(msg.sender) {}
|
||||
|
||||
function setExecutor(address executor, bool trusted) external onlyOwner {
|
||||
@@ -39,20 +35,4 @@ contract GiftedAccountGuardian is Ownable2Step, IGiftedAccountGuardian {
|
||||
if (executor == owner()) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// @dev this function can only be called by the owner of the token,
|
||||
// not by anyone else, even if they are an executor, which yield the
|
||||
// control to the token holder.
|
||||
function setCustomAccountImplementation(address account, address implementation) external {
|
||||
require(
|
||||
implementation != address(0) && implementation.code.length != 0, "!implementation-is-not-a-contract-or-zero"
|
||||
);
|
||||
require(IGiftedAccount(account).isOwner(msg.sender));
|
||||
_customAccountImplementation[account] = implementation;
|
||||
emit CustomAccountImplementationUpdated(account, implementation);
|
||||
}
|
||||
|
||||
function getCustomAccountImplementation(address account) external view returns (address) {
|
||||
return _customAccountImplementation[account];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,11 +22,6 @@ contract GiftedAccountProxy is Proxy {
|
||||
// the override can only be controlled by the token holder, not the proxy itself or account guardian.
|
||||
// @return address of the implementation
|
||||
function _implementation() internal view virtual override returns (address) {
|
||||
address customImpl = _accountGuardian.getCustomAccountImplementation(address(this));
|
||||
if (customImpl != address(0)) {
|
||||
return customImpl;
|
||||
}
|
||||
|
||||
return _accountGuardian.getImplementation();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Compatible with OpenZeppelin Contracts ^5.0.0
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import "@openzeppelin/utils/Address.sol";
|
||||
import "@openzeppelin-contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol";
|
||||
import "@openzeppelin-contracts-upgradeable/token/ERC721/extensions/ERC721PausableUpgradeable.sol";
|
||||
import "@openzeppelin-contracts-upgradeable/access/AccessControlUpgradeable.sol";
|
||||
@@ -9,9 +9,15 @@ import "@openzeppelin-contracts-upgradeable/token/ERC721/extensions/ERC721Burnab
|
||||
import "@openzeppelin-contracts-upgradeable/proxy/utils/Initializable.sol";
|
||||
import "@openzeppelin-contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
|
||||
import "@openzeppelin-contracts-upgradeable/token/ERC721/utils/ERC721HolderUpgradeable.sol";
|
||||
import {GiftedAccount} from "./GiftedAccount.sol";
|
||||
import "./GiftedAccountGuardian.sol";
|
||||
import "./interfaces/IGasSponsorBook.sol";
|
||||
import "./interfaces/IGiftedBox.sol";
|
||||
import "erc6551/ERC6551Registry.sol";
|
||||
|
||||
/// @custom:security-contact zitao@placeholdersoft.com
|
||||
contract GiftedBox is
|
||||
IGiftedBox,
|
||||
Initializable,
|
||||
ERC721HolderUpgradeable,
|
||||
ERC721Upgradeable,
|
||||
@@ -20,25 +26,58 @@ contract GiftedBox is
|
||||
ERC721BurnableUpgradeable,
|
||||
UUPSUpgradeable
|
||||
{
|
||||
using Address for address payable;
|
||||
// region defines
|
||||
bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");
|
||||
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
|
||||
bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE");
|
||||
bytes32 public constant CLAIM_ADMIN_ROLE = keccak256("CLAIM_ADMIN_ROLE");
|
||||
|
||||
struct GiftingRecord {
|
||||
address sender;
|
||||
address recipient;
|
||||
}
|
||||
event GiftedBoxSentToVault(
|
||||
address indexed from,
|
||||
address indexed to,
|
||||
uint256 tokenId
|
||||
);
|
||||
event GiftedBoxClaimed(
|
||||
uint256 tokenId,
|
||||
address indexed claimer,
|
||||
bool asSender
|
||||
);
|
||||
event GiftedBoxClaimedByAdmin(
|
||||
uint256 tokenId,
|
||||
address indexed claimer,
|
||||
bool asSender,
|
||||
address indexed admin
|
||||
);
|
||||
|
||||
event GiftedBoxSentToVault(address indexed from, address indexed to, uint256 tokenId);
|
||||
event GiftBoxClaimed(uint256 tokenId, address indexed claimer, bool asSender);
|
||||
event GiftBoxClaimedByAdmin(uint256 tokenId, address indexed claimer, bool asSender, address indexed admin);
|
||||
event AccountImplUpdated(address indexed newAccountImpl);
|
||||
event RegistryUpdated(address indexed newRegistry);
|
||||
event GuardianUpdated(address indexed newGuardian);
|
||||
event GasSponsorBookUpdated(address indexed newGasSponsorBook);
|
||||
event TransferEtherToAccount(
|
||||
address indexed account,
|
||||
address indexed from,
|
||||
uint256 value
|
||||
);
|
||||
event SponsorEnabled(
|
||||
address indexed account,
|
||||
uint256 tokenId,
|
||||
uint256 ticket
|
||||
);
|
||||
event SponsorTicketAdded(
|
||||
address indexed account,
|
||||
uint256 ticket,
|
||||
uint256 value
|
||||
);
|
||||
// endregion
|
||||
|
||||
// region storage
|
||||
uint256 private _nextTokenId;
|
||||
mapping(uint256 => GiftingRecord) public giftingRecords;
|
||||
GiftedAccount public accountImpl;
|
||||
ERC6551Registry public registry;
|
||||
GiftedAccountGuardian public guardian;
|
||||
IGasSponsorBook public gasSponsorBook;
|
||||
|
||||
// endregion
|
||||
|
||||
@@ -49,7 +88,7 @@ contract GiftedBox is
|
||||
}
|
||||
|
||||
function initialize(address defaultAdmin) public initializer {
|
||||
__ERC721_init("GiftBoxV2", "GB");
|
||||
__ERC721_init("GiftedBoxV2", "GB");
|
||||
__ERC721Pausable_init();
|
||||
__AccessControl_init();
|
||||
__ERC721Burnable_init();
|
||||
@@ -70,12 +109,18 @@ contract GiftedBox is
|
||||
_unpause();
|
||||
}
|
||||
|
||||
function _authorizeUpgrade(address newImplementation) internal override onlyRole(UPGRADER_ROLE) {}
|
||||
function _authorizeUpgrade(
|
||||
address newImplementation
|
||||
) internal override onlyRole(UPGRADER_ROLE) {}
|
||||
|
||||
// endregion
|
||||
|
||||
// region overrides
|
||||
function _update(address to, uint256 tokenId, address auth)
|
||||
function _update(
|
||||
address to,
|
||||
uint256 tokenId,
|
||||
address auth
|
||||
)
|
||||
internal
|
||||
override(ERC721Upgradeable, ERC721PausableUpgradeable)
|
||||
returns (address)
|
||||
@@ -83,7 +128,9 @@ contract GiftedBox is
|
||||
return super._update(to, tokenId, auth);
|
||||
}
|
||||
|
||||
function supportsInterface(bytes4 interfaceId)
|
||||
function supportsInterface(
|
||||
bytes4 interfaceId
|
||||
)
|
||||
public
|
||||
view
|
||||
override(ERC721Upgradeable, AccessControlUpgradeable)
|
||||
@@ -94,6 +141,140 @@ contract GiftedBox is
|
||||
|
||||
// endregion
|
||||
|
||||
// region config
|
||||
function setAccountImpl(
|
||||
address payable newAccountImpl
|
||||
) public onlyRole(DEFAULT_ADMIN_ROLE) {
|
||||
accountImpl = GiftedAccount(newAccountImpl);
|
||||
emit AccountImplUpdated(address(newAccountImpl));
|
||||
}
|
||||
|
||||
function setRegistry(
|
||||
address newRegistry
|
||||
) public onlyRole(DEFAULT_ADMIN_ROLE) {
|
||||
registry = ERC6551Registry(newRegistry);
|
||||
emit RegistryUpdated(address(newRegistry));
|
||||
}
|
||||
|
||||
function setAccountGuardian(
|
||||
address newGuardian
|
||||
) public onlyRole(DEFAULT_ADMIN_ROLE) {
|
||||
guardian = GiftedAccountGuardian(newGuardian);
|
||||
emit GuardianUpdated(address(newGuardian));
|
||||
}
|
||||
|
||||
function setGasSponsorBook(
|
||||
address newGasSponsorBook
|
||||
) public onlyRole(DEFAULT_ADMIN_ROLE) {
|
||||
gasSponsorBook = IGasSponsorBook(newGasSponsorBook);
|
||||
emit GasSponsorBookUpdated(address(newGasSponsorBook));
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region view
|
||||
function tokenAccountAddress(
|
||||
uint256 tokenId
|
||||
) public view returns (address) {
|
||||
return
|
||||
registry.account(
|
||||
address(accountImpl),
|
||||
block.chainid,
|
||||
address(this),
|
||||
tokenId,
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
function generateTicketID(address account) public pure returns (uint256) {
|
||||
return uint256(keccak256(abi.encodePacked(account)));
|
||||
}
|
||||
function getGiftingRecord(
|
||||
uint256 tokenId
|
||||
) public view returns (GiftingRecord memory) {
|
||||
return giftingRecords[tokenId];
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region internal functions
|
||||
function createAccountIfNeeded(uint256 tokenId, address tokenAccount) internal {
|
||||
if (tokenAccount.code.length == 0) {
|
||||
registry.createAccount(
|
||||
address(accountImpl),
|
||||
block.chainid,
|
||||
address(this),
|
||||
tokenId,
|
||||
0,
|
||||
abi.encodeWithSignature("initialize(address)", address(guardian))
|
||||
);
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region gas sponsorship
|
||||
function handleSponsorshipAndTransfer(
|
||||
address tokenAccount,
|
||||
uint256 tokenId
|
||||
) internal {
|
||||
if (
|
||||
address(gasSponsorBook) != address(0) &&
|
||||
msg.value >= gasSponsorBook.feePerSponsorTicket()
|
||||
) {
|
||||
uint256 sponserFee = gasSponsorBook.feePerSponsorTicket();
|
||||
uint256 ticket = generateTicketID(address(tokenAccount));
|
||||
gasSponsorBook.addSponsorTicket{value: sponserFee}(ticket);
|
||||
uint256 left = msg.value - sponserFee;
|
||||
if (left > 0) {
|
||||
payable(tokenAccount).sendValue(left);
|
||||
emit TransferEtherToAccount(tokenAccount, msg.sender, left);
|
||||
}
|
||||
emit SponsorEnabled(tokenAccount, tokenId, ticket);
|
||||
} else if (msg.value > 0) {
|
||||
uint256 value = msg.value;
|
||||
emit TransferEtherToAccount(tokenAccount, msg.sender, value);
|
||||
payable(tokenAccount).sendValue(value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a sponsor ticket for the given account and token ID, paying the sponsor ticket fee.
|
||||
* A sponsor ticket allows the account holder to sponsor a gas refund for transfers of the token ID.
|
||||
* The sponsor ticket ID is generated and stored in the gas sponsor book along with the sponsor funds.
|
||||
* Emits a SponsorTicketAdded event with details.
|
||||
*/
|
||||
function addSponsorTicket(address account) external payable {
|
||||
require(
|
||||
msg.value >= gasSponsorBook.feePerSponsorTicket(),
|
||||
"Insufficient funds for sponsor ticket"
|
||||
);
|
||||
uint256 ticket = generateTicketID(account);
|
||||
gasSponsorBook.addSponsorTicket{value: msg.value}(ticket);
|
||||
emit SponsorTicketAdded(account, ticket, msg.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Checks if a given NFT token has a sponsor ticket.
|
||||
* @param tokenId The ID of the NFT token.
|
||||
* @return A boolean indicating whether the NFT token has a sponsor ticket or not.
|
||||
*/
|
||||
function hasSponsorTicket(uint256 tokenId) public view returns (bool) {
|
||||
if (address(gasSponsorBook) == address(0)) {
|
||||
return false;
|
||||
}
|
||||
address tokenAccount = registry.account(
|
||||
address(accountImpl),
|
||||
block.chainid,
|
||||
address(this),
|
||||
tokenId,
|
||||
0
|
||||
);
|
||||
uint256 ticket = generateTicketID(tokenAccount);
|
||||
return gasSponsorBook.sponsorTickets(ticket) > 0;
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region Gifting Actions
|
||||
|
||||
/**
|
||||
@@ -101,14 +282,24 @@ contract GiftedBox is
|
||||
* @dev Mints a new token, updates the gifting records, and emits an event.
|
||||
* @param recipient The address of the recipient who will receive the gift.
|
||||
*/
|
||||
function sendGift(address recipient) public {
|
||||
function sendGift(address recipient) public payable whenNotPaused {
|
||||
uint256 tokenId = _nextTokenId++;
|
||||
_safeMint(recipient, tokenId);
|
||||
_update(address(this), tokenId, recipient);
|
||||
|
||||
giftingRecords[tokenId] = GiftingRecord({sender: msg.sender, recipient: recipient});
|
||||
giftingRecords[tokenId] = GiftingRecord({
|
||||
sender: msg.sender,
|
||||
recipient: recipient
|
||||
});
|
||||
|
||||
|
||||
address tokenAccount = registry.account(address(accountImpl), block.chainid, address(this), tokenId, 0);
|
||||
createAccountIfNeeded(tokenId, tokenAccount);
|
||||
handleSponsorshipAndTransfer(tokenAccount, tokenId);
|
||||
|
||||
emit GiftedBoxSentToVault(msg.sender, recipient, tokenId);
|
||||
// 1 -> sendGift : got tokenId + address 100
|
||||
// 2 -> transferNFT to tokenId's address: 100
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -119,7 +310,10 @@ contract GiftedBox is
|
||||
function resendGift(uint256 tokenId, address recipient) public {
|
||||
safeTransferFrom(address(msg.sender), address(this), tokenId);
|
||||
|
||||
giftingRecords[tokenId] = GiftingRecord({sender: msg.sender, recipient: recipient});
|
||||
giftingRecords[tokenId] = GiftingRecord({
|
||||
sender: msg.sender,
|
||||
recipient: recipient
|
||||
});
|
||||
|
||||
emit GiftedBoxSentToVault(msg.sender, recipient, tokenId);
|
||||
}
|
||||
@@ -139,7 +333,7 @@ contract GiftedBox is
|
||||
|
||||
delete giftingRecords[tokenId];
|
||||
_update(msg.sender, tokenId, address(this));
|
||||
emit GiftBoxClaimed(tokenId, msg.sender, asSender);
|
||||
emit GiftedBoxClaimed(tokenId, msg.sender, asSender);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -148,7 +342,11 @@ contract GiftedBox is
|
||||
* @param claimer The address of the user claiming the gift.
|
||||
* @param toSender A boolean indicating if the gift should be claimed to the sender.
|
||||
*/
|
||||
function claimGiftByAdmin(uint256 tokenId, address claimer, bool toSender) public onlyRole(CLAIM_ADMIN_ROLE) {
|
||||
function claimGiftByAdmin(
|
||||
uint256 tokenId,
|
||||
address claimer,
|
||||
bool toSender
|
||||
) public onlyRole(CLAIM_ADMIN_ROLE) {
|
||||
GiftingRecord memory record = giftingRecords[tokenId];
|
||||
if (toSender) {
|
||||
require(record.sender == claimer, "!not-sender");
|
||||
@@ -158,9 +356,8 @@ contract GiftedBox is
|
||||
|
||||
delete giftingRecords[tokenId];
|
||||
_update(claimer, tokenId, address(this));
|
||||
emit GiftBoxClaimed(tokenId, claimer, toSender);
|
||||
emit GiftBoxClaimedByAdmin(tokenId, claimer, toSender, msg.sender);
|
||||
emit GiftedBoxClaimed(tokenId, claimer, toSender);
|
||||
emit GiftedBoxClaimedByAdmin(tokenId, claimer, toSender, msg.sender);
|
||||
}
|
||||
|
||||
// endregion
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-License-Identifier: UNLICENSED
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
interface IGiftedAccountGuardian {
|
||||
@@ -10,7 +10,4 @@ interface IGiftedAccountGuardian {
|
||||
function setExecutor(address executor, bool trusted) external;
|
||||
|
||||
function getImplementation() external view returns (address);
|
||||
|
||||
function getCustomAccountImplementation(address account) external view returns (address);
|
||||
function setGiftedAccountImplementation(address newImplementation) external;
|
||||
}
|
||||
|
||||
12
src/interfaces/IGiftedBox.sol
Normal file
12
src/interfaces/IGiftedBox.sol
Normal file
@@ -0,0 +1,12 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.20;
|
||||
struct GiftingRecord {
|
||||
address sender;
|
||||
address recipient;
|
||||
}
|
||||
|
||||
interface IGiftedBox {
|
||||
function getGiftingRecord(
|
||||
uint256 tokenId
|
||||
) external view returns (GiftingRecord memory);
|
||||
}
|
||||
@@ -6,7 +6,12 @@ import "forge-std/console.sol";
|
||||
import "forge-std/Vm.sol";
|
||||
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
|
||||
import "../src/GiftedBox.sol";
|
||||
import {GiftedAccount, IERC6551Account} from "../src/GiftedAccount.sol";
|
||||
import "../src/GiftedAccountGuardian.sol";
|
||||
import "../src/GiftedAccountProxy.sol";
|
||||
import "erc6551/ERC6551Registry.sol";
|
||||
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
|
||||
import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";
|
||||
import "@openzeppelin-contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol";
|
||||
import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
|
||||
|
||||
@@ -18,22 +23,67 @@ contract MockERC721 is ERC721 {
|
||||
}
|
||||
}
|
||||
|
||||
contract MockERC1155 is ERC1155 {
|
||||
constructor() ERC1155("") {}
|
||||
|
||||
function mint(address to, uint256 tokenId, uint256 amount) public {
|
||||
_mint(to, tokenId, amount, "");
|
||||
}
|
||||
|
||||
function mintBatch(
|
||||
address to,
|
||||
uint256[] memory tokenIds,
|
||||
uint256[] memory amounts
|
||||
) public {
|
||||
_mintBatch(to, tokenIds, amounts, "");
|
||||
}
|
||||
}
|
||||
|
||||
interface TestEvents {
|
||||
event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
|
||||
event Transfer(
|
||||
address indexed from,
|
||||
address indexed to,
|
||||
uint256 indexed tokenId
|
||||
);
|
||||
}
|
||||
|
||||
contract GiftedBoxTest is Test, TestEvents {
|
||||
MockERC721 mockNFT;
|
||||
GiftedBox giftedBox;
|
||||
MockERC721 internal mockNFT;
|
||||
MockERC1155 internal mockERC1155;
|
||||
GiftedBox internal giftedBox;
|
||||
ERC6551Registry internal registry;
|
||||
GiftedAccountGuardian internal guardian = new GiftedAccountGuardian();
|
||||
GiftedAccount internal giftedAccount;
|
||||
|
||||
// region Setup
|
||||
function setUp() public {
|
||||
mockNFT = new MockERC721();
|
||||
mockERC1155 = new MockERC1155();
|
||||
|
||||
GiftedAccount giftedAccountImpl = new GiftedAccount();
|
||||
guardian.setGiftedAccountImplementation(address(giftedAccountImpl));
|
||||
|
||||
GiftedAccountProxy accountProxy = new GiftedAccountProxy(
|
||||
address(guardian)
|
||||
);
|
||||
giftedAccount = GiftedAccount(payable(address(accountProxy)));
|
||||
|
||||
registry = new ERC6551Registry();
|
||||
|
||||
address implementation = address(new GiftedBox());
|
||||
bytes memory data = abi.encodeCall(GiftedBox.initialize, address(this));
|
||||
address proxy = address(new ERC1967Proxy(implementation, data));
|
||||
giftedBox = GiftedBox(proxy);
|
||||
|
||||
giftedBox.setAccountImpl(payable(address(giftedAccount)));
|
||||
giftedBox.setRegistry(address(registry));
|
||||
giftedBox.setGasSponsorBook(address(0));
|
||||
giftedBox.setAccountGuardian(address(guardian));
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region Gifting Actions
|
||||
function testSendGift() public {
|
||||
uint256 tokenId = 0;
|
||||
address giftSender = vm.addr(1);
|
||||
@@ -95,7 +145,9 @@ contract GiftedBoxTest is Test, TestEvents {
|
||||
|
||||
assertEq(giftSender, IERC721(giftedBox).ownerOf(tokenId));
|
||||
{
|
||||
(address sender, address recipient) = giftedBox.giftingRecords(tokenId);
|
||||
(address sender, address recipient) = giftedBox.giftingRecords(
|
||||
tokenId
|
||||
);
|
||||
assertEq(sender, address(0));
|
||||
assertEq(recipient, address(0));
|
||||
}
|
||||
@@ -111,7 +163,9 @@ contract GiftedBoxTest is Test, TestEvents {
|
||||
giftedBox.claimGiftByAdmin(tokenId, giftRecipient, false);
|
||||
assertEq(giftRecipient, IERC721(giftedBox).ownerOf(tokenId));
|
||||
{
|
||||
(address sender, address recipient) = giftedBox.giftingRecords(tokenId);
|
||||
(address sender, address recipient) = giftedBox.giftingRecords(
|
||||
tokenId
|
||||
);
|
||||
assertEq(sender, address(0));
|
||||
assertEq(recipient, address(0));
|
||||
}
|
||||
@@ -133,7 +187,9 @@ contract GiftedBoxTest is Test, TestEvents {
|
||||
|
||||
assertEq(address(giftedBox), IERC721(giftedBox).ownerOf(tokenId));
|
||||
{
|
||||
(address sender, address recipient) = giftedBox.giftingRecords(tokenId);
|
||||
(address sender, address recipient) = giftedBox.giftingRecords(
|
||||
tokenId
|
||||
);
|
||||
assertEq(sender, giftSender);
|
||||
assertEq(recipient, giftRecipient);
|
||||
}
|
||||
@@ -141,9 +197,170 @@ contract GiftedBoxTest is Test, TestEvents {
|
||||
vm.prank(giftRecipient);
|
||||
giftedBox.claimGift(tokenId, false);
|
||||
{
|
||||
(address sender, address recipient) = giftedBox.giftingRecords(tokenId);
|
||||
(address sender, address recipient) = giftedBox.giftingRecords(
|
||||
tokenId
|
||||
);
|
||||
assertEq(address(0), sender);
|
||||
assertEq(address(0), recipient);
|
||||
}
|
||||
}
|
||||
|
||||
// endregion Gifting Actions
|
||||
|
||||
// region TokenBound Account
|
||||
|
||||
function testTokenBoundAccountCallDirectly() public {
|
||||
uint256 tokenId = 0;
|
||||
address giftSender = vm.addr(1);
|
||||
address giftRecipient = vm.addr(2);
|
||||
|
||||
vm.prank(giftSender);
|
||||
giftedBox.sendGift(giftRecipient);
|
||||
|
||||
address account = registry.account(
|
||||
address(giftedAccount),
|
||||
block.chainid,
|
||||
address(giftedBox),
|
||||
tokenId,
|
||||
0
|
||||
);
|
||||
|
||||
address tokenAccount = giftedBox.tokenAccountAddress(tokenId);
|
||||
assertTrue(account != address(0));
|
||||
assertTrue(account != vm.addr(1));
|
||||
assertEq(account, tokenAccount);
|
||||
|
||||
IERC6551Account accountInstance = IERC6551Account(payable(account));
|
||||
|
||||
vm.prank(vm.addr(1));
|
||||
giftedBox.claimGift(tokenId, true);
|
||||
|
||||
assertEq(accountInstance.owner(), vm.addr(1));
|
||||
|
||||
vm.deal(account, 1 ether);
|
||||
|
||||
vm.prank(vm.addr(1));
|
||||
accountInstance.executeCall(payable(vm.addr(2)), 0.5 ether, "");
|
||||
|
||||
assertEq(account.balance, 0.5 ether);
|
||||
assertEq(vm.addr(2).balance, 0.5 ether);
|
||||
assertEq(accountInstance.nonce(), 1);
|
||||
|
||||
vm.prank(vm.addr(1));
|
||||
giftedBox.transferFrom(vm.addr(1), vm.addr(2), tokenId);
|
||||
assertEq(accountInstance.owner(), vm.addr(2));
|
||||
|
||||
vm.prank(vm.addr(1));
|
||||
vm.expectRevert();
|
||||
accountInstance.executeCall(payable(vm.addr(2)), 0.5 ether, "");
|
||||
|
||||
vm.prank(vm.addr(2));
|
||||
accountInstance.executeCall(payable(vm.addr(2)), 0.5 ether, "");
|
||||
assertEq(vm.addr(2).balance, 1 ether);
|
||||
assertEq(accountInstance.nonce(), 2);
|
||||
}
|
||||
|
||||
function testTokenBoundAccountERC721() public {
|
||||
uint256 tokenId = 0;
|
||||
address giftSender = vm.addr(1);
|
||||
address giftRecipient = vm.addr(2);
|
||||
address randomAccount = vm.addr(3);
|
||||
|
||||
vm.prank(giftSender);
|
||||
giftedBox.sendGift(giftRecipient);
|
||||
|
||||
address tokenAccount = giftedBox.tokenAccountAddress(tokenId);
|
||||
|
||||
// !important: only the token owner can transfer the token to tokenBound account
|
||||
// to prevent front running attack
|
||||
mockNFT.mint(randomAccount, 100);
|
||||
vm.expectRevert("!sender-not-authorized");
|
||||
vm.prank(randomAccount);
|
||||
mockNFT.safeTransferFrom(randomAccount, tokenAccount, 100);
|
||||
|
||||
// sender is able to transfer the token to tokenBound account
|
||||
mockNFT.mint(giftSender, 101);
|
||||
vm.prank(giftSender);
|
||||
mockNFT.safeTransferFrom(giftSender, tokenAccount, 101);
|
||||
|
||||
// recipient is not able to transfer the token to tokenBound account
|
||||
mockNFT.mint(giftRecipient, 102);
|
||||
vm.expectRevert("!sender-not-authorized");
|
||||
vm.prank(giftRecipient);
|
||||
mockNFT.safeTransferFrom(giftRecipient, tokenAccount, 102);
|
||||
}
|
||||
|
||||
function testTokenBoundAccountERC1155() public {
|
||||
uint256 tokenId = 0;
|
||||
address giftSender = vm.addr(1);
|
||||
address giftRecipient = vm.addr(2);
|
||||
address randomAccount = vm.addr(3);
|
||||
|
||||
vm.prank(giftSender);
|
||||
giftedBox.sendGift(giftRecipient);
|
||||
|
||||
address tokenAccount = giftedBox.tokenAccountAddress(tokenId);
|
||||
|
||||
// !important: only the token owner can transfer the token to tokenBound account
|
||||
// to prevent front running attack
|
||||
mockERC1155.mint(randomAccount, 100, 1);
|
||||
vm.expectRevert("!sender-not-authorized");
|
||||
vm.prank(randomAccount);
|
||||
mockERC1155.safeTransferFrom(randomAccount, tokenAccount, 100, 1, "");
|
||||
|
||||
// sender is able to transfer the token to tokenBound account
|
||||
mockERC1155.mint(giftSender, 101, 1);
|
||||
vm.prank(giftSender);
|
||||
mockERC1155.safeTransferFrom(giftSender, tokenAccount, 101, 1, "");
|
||||
|
||||
// recipient is not able to transfer the token to tokenBound account
|
||||
mockERC1155.mint(giftRecipient, 102, 1);
|
||||
vm.expectRevert("!sender-not-authorized");
|
||||
vm.prank(giftRecipient);
|
||||
mockERC1155.safeTransferFrom(giftRecipient, tokenAccount, 102, 1, "");
|
||||
|
||||
// Test cases for onERC1155BatchReceived
|
||||
uint256[] memory ids = new uint256[](2);
|
||||
uint256[] memory amounts = new uint256[](2);
|
||||
ids[0] = 200;
|
||||
ids[1] = 201;
|
||||
amounts[0] = 1;
|
||||
amounts[1] = 2;
|
||||
|
||||
// randomAccount tries to transfer batch tokens to tokenBound account
|
||||
mockERC1155.mintBatch(randomAccount, ids, amounts);
|
||||
vm.expectRevert("!sender-not-authorized");
|
||||
vm.prank(randomAccount);
|
||||
mockERC1155.safeBatchTransferFrom(
|
||||
randomAccount,
|
||||
tokenAccount,
|
||||
ids,
|
||||
amounts,
|
||||
""
|
||||
);
|
||||
|
||||
// giftSender transfers batch tokens to tokenBound account
|
||||
mockERC1155.mintBatch(giftSender, ids, amounts);
|
||||
vm.prank(giftSender);
|
||||
mockERC1155.safeBatchTransferFrom(
|
||||
giftSender,
|
||||
tokenAccount,
|
||||
ids,
|
||||
amounts,
|
||||
""
|
||||
);
|
||||
|
||||
// giftRecipient tries to transfer batch tokens to tokenBound account
|
||||
mockERC1155.mintBatch(giftRecipient, ids, amounts);
|
||||
vm.expectRevert("!sender-not-authorized");
|
||||
vm.prank(giftRecipient);
|
||||
mockERC1155.safeBatchTransferFrom(
|
||||
giftRecipient,
|
||||
tokenAccount,
|
||||
ids,
|
||||
amounts,
|
||||
""
|
||||
);
|
||||
}
|
||||
// endregion TokenBound Account
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user