diff --git a/contracts/misc/NewbieVilla.sol b/contracts/misc/NewbieVilla.sol index 596c59e5..b1b7d09f 100644 --- a/contracts/misc/NewbieVilla.sol +++ b/contracts/misc/NewbieVilla.sol @@ -14,7 +14,7 @@ import {IERC777} from "@openzeppelin/contracts/token/ERC777/IERC777.sol"; import {IERC1820Registry} from "@openzeppelin/contracts/utils/introspection/IERC1820Registry.sol"; /** - * @dev Implementation of a contract to keep characters for others. The address with + * @dev Implementation of a contract to keep characters for others. The keepers and addresses with * the ADMIN_ROLE are expected to issue the proof to users. Then users could use the * proof to withdraw the corresponding character. */ @@ -33,6 +33,8 @@ contract NewbieVilla is Initializable, AccessControlEnumerable, IERC721Receiver, mapping(uint256 => uint256) internal _balances; address internal _tips; // tips contract + mapping(uint256 characterId => address keeper) private _keepers; + // events /** * @dev Emitted when the web3Entry character nft is withdrawn. @@ -82,7 +84,7 @@ contract NewbieVilla is Initializable, AccessControlEnumerable, IERC721Receiver, /** * @notice Tips a character by transferring `amount` tokens - * from account with `ADMIN_ROLE` to `Tips` contract.
+ * from account with required permission to `Tips` contract.
* * Admin will call `send` erc777 token to the Tips contract, with `fromCharacterId` * and `toCharacterId` encoded in the `data`.
@@ -93,14 +95,17 @@ contract NewbieVilla is Initializable, AccessControlEnumerable, IERC721Receiver, * [AbiCoder-encode](https://docs.ethers.org/v5/api/utils/abi/coder/#AbiCoder-encode).
* * Requirements: - * - The `msg.sender` must have `ADMIN_ROLE`. + * - The `msg.sender` must be character's keeper or have `ADMIN_ROLE` but not character's keeper. * @param fromCharacterId The token ID of character that calls this contract. * @param toCharacterId The token ID of character that will receive the token. * @param amount Amount of token. */ function tipCharacter(uint256 fromCharacterId, uint256 toCharacterId, uint256 amount) external { - // check admin role - require(hasRole(ADMIN_ROLE, msg.sender), "NewbieVilla: unauthorized role for tipCharacter"); + // check permission + require( + _hasPermission(msg.sender, fromCharacterId), + "NewbieVilla: unauthorized role for tipCharacter" + ); // newbievilla's balance - tip amount // will fail if balance is insufficient @@ -115,7 +120,7 @@ contract NewbieVilla is Initializable, AccessControlEnumerable, IERC721Receiver, /** * @notice Tips a character's note by transferring `amount` tokens - * from account with `ADMIN_ROLE` to `Tips` contract.
+ * from account with required permission to `Tips` contract.
* * Admin will call `send` erc777 token to the Tips contract, with `fromCharacterId`, * `toCharacterId` and `toNoteId` encoded in the `data`.
@@ -126,7 +131,7 @@ contract NewbieVilla is Initializable, AccessControlEnumerable, IERC721Receiver, * [AbiCoder-encode](https://docs.ethers.org/v5/api/utils/abi/coder/#AbiCoder-encode).
* * Requirements: - * - The `msg.sender` must have `ADMIN_ROLE`. + * - The `msg.sender` must be character's keeper or have `ADMIN_ROLE` but not character's keeper. * @param fromCharacterId The token ID of character that calls this contract. * @param toCharacterId The token ID of character that will receive the token. * @param toNoteId The note ID. @@ -138,9 +143,9 @@ contract NewbieVilla is Initializable, AccessControlEnumerable, IERC721Receiver, uint256 toNoteId, uint256 amount ) external { - // check admin role + // check permission require( - hasRole(ADMIN_ROLE, msg.sender), + _hasPermission(msg.sender, fromCharacterId), "NewbieVilla: unauthorized role for tipCharacterForNote" ); @@ -158,7 +163,7 @@ contract NewbieVilla is Initializable, AccessControlEnumerable, IERC721Receiver, /** * @notice Withdraw character#`characterId` to `to` using the nonce, expires and the proof.
* Emits the `Withdraw` event.
- * @dev Proof is the signature from someone with the ADMIN_ROLE. The message to sign is + * @dev Proof is the signature from character keepers or someone with the ADMIN_ROLE. The message to sign is * the packed data of this contract's address, `characterId`, `nonce` and `expires`.
* * Here's an example to generate a proof:
@@ -174,7 +179,7 @@ contract NewbieVilla is Initializable, AccessControlEnumerable, IERC721Receiver, * * Requirements: : * - `expires` is greater than the current timestamp - * - `proof` is signed by the one with the ADMIN_ROLE + * - `proof` is signed by the keeper or address with the ADMIN_ROLE * * @param to Receiver of the withdrawn character. * @param characterId The token id of the character to withdraw. @@ -192,13 +197,17 @@ contract NewbieVilla is Initializable, AccessControlEnumerable, IERC721Receiver, bytes32 signedData = ECDSA.toEthSignedMessageHash( keccak256(abi.encodePacked(address(this), characterId, nonce, expires)) ); - require( - hasRole(ADMIN_ROLE, ECDSA.recover(signedData, proof)), - "NewbieVilla: unauthorized withdraw" - ); + // check proof + address signer = ECDSA.recover(signedData, proof); + require(_hasPermission(signer, characterId), "NewbieVilla: unauthorized withdraw"); + + // update balance uint256 amount = _balances[characterId]; _balances[characterId] = 0; + // update keeper + delete _keepers[characterId]; + // send token IERC777(_token).send(to, amount, ""); // solhint-disable-line check-send-result @@ -217,7 +226,6 @@ contract NewbieVilla is Initializable, AccessControlEnumerable, IERC721Receiver, * Requirements: : * * - `msg.sender` must be address of Web3Entry. - * - `operator` must has ADMIN_ROLE. * * @param data bytes encoded from the operator address to set for the incoming character. * @@ -230,9 +238,11 @@ contract NewbieVilla is Initializable, AccessControlEnumerable, IERC721Receiver, ) external override returns (bytes4) { // Only character nft could be received, other nft, e.g. mint nft would be reverted require(msg.sender == web3Entry, "NewbieVilla: receive unknown token"); - // Only admin role could send character to this contract - require(hasRole(ADMIN_ROLE, operator), "NewbieVilla: receive unknown character"); + // set keeper for tokenId + _keepers[tokenId] = operator; + + // grant operator permissions if (data.length == 0) { IWeb3Entry(web3Entry).grantOperatorPermissions( tokenId, @@ -295,6 +305,15 @@ contract NewbieVilla is Initializable, AccessControlEnumerable, IERC721Receiver, return _balances[characterId]; } + /** + * @notice Returns the address of keeper by `characterId`. + * @param characterId The character ID to query. + * @return address The address of the keeper. + */ + function getKeeper(uint256 characterId) external view returns (address) { + return _keepers[characterId]; + } + /** * @notice Returns the address of mira token contract. * @return The address of mira token contract. @@ -302,4 +321,10 @@ contract NewbieVilla is Initializable, AccessControlEnumerable, IERC721Receiver, function getToken() external view returns (address) { return _token; } + + /// @dev It will return true if `account` is character's keeper or has `ADMIN_ROLE` but not character's keeper. + function _hasPermission(address account, uint256 characterId) internal view returns (bool) { + address keeper = _keepers[characterId]; + return (keeper == account) || (keeper == address(0) && hasRole(ADMIN_ROLE, account)); + } } diff --git a/scripts/Deployer.sol b/scripts/Deployer.sol index 5ddfe9cc..6062b413 100644 --- a/scripts/Deployer.sol +++ b/scripts/Deployer.sol @@ -334,7 +334,7 @@ abstract contract Deployer is Script { return string(res); } - /// @notice Returns the constructor arguent of a deployment transaction given a transaction json. + /// @notice Returns the constructor arguments of a deployment transaction given a transaction json. function getDeployTransactionConstructorArguments( string memory _transaction ) internal returns (string[] memory) { diff --git a/test/UpgradeNewbieVilla.t.sol b/test/UpgradeNewbieVilla.t.sol new file mode 100644 index 00000000..72785a53 --- /dev/null +++ b/test/UpgradeNewbieVilla.t.sol @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: MIT +// solhint-disable comprehensive-interface +pragma solidity 0.8.18; + +import {CommonTest} from "./helpers/CommonTest.sol"; +import {NewbieVilla} from "../contracts/misc/NewbieVilla.sol"; +import { + TransparentUpgradeableProxy +} from "../contracts/upgradeability/TransparentUpgradeableProxy.sol"; +import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; + +contract UpgradeNewbieVillaTest is CommonTest { + // test upgradeability of NewbieVilla from crossbell fork + address internal _web3Entry = address(0xa6f969045641Cf486a747A2688F3a5A6d43cd0D8); + address internal constant _token = 0xAfB95CC0BD320648B3E8Df6223d9CDD05EbeDC64; + address payable internal _newbieVilla = + payable(address(0xD0c83f0BB2c61D55B3d33950b70C59ba2f131caA)); + address internal _proxyAdmin = address(0x5f603895B48F0C451af39bc7e0c587aE15718e4d); + + function setUp() public { + // create and select a fork from crossbell at block 46718115 + vm.createSelectFork(vm.envString("CROSSBELL_RPC_URL"), 46718115); + } + + function testCheckSetupState() public { + assertEq(NewbieVilla(_newbieVilla).web3Entry(), _web3Entry); + assertEq(NewbieVilla(_newbieVilla).getToken(), _token); + } + + function testUpgradeNewbieVilla() public { + NewbieVilla newImpl = new NewbieVilla(); + // upgrade and initialize + vm.prank(_proxyAdmin); + TransparentUpgradeableProxy(_newbieVilla).upgradeTo(address(newImpl)); + // check newImpl + vm.prank(_proxyAdmin); + assertEq(TransparentUpgradeableProxy(_newbieVilla).implementation(), address(newImpl)); + // check state + assertEq(NewbieVilla(_newbieVilla).web3Entry(), _web3Entry); + assertEq(NewbieVilla(_newbieVilla).getToken(), _token); + } + + function testUpgradeNewbieVillaWithStorageCheck() public { + // create and select a fork from crossbell at block 46718115 + vm.createSelectFork(vm.envString("CROSSBELL_RPC_URL"), 46718115); + + NewbieVilla newImpl = new NewbieVilla(); + // upgrade + vm.prank(_proxyAdmin); + TransparentUpgradeableProxy(_newbieVilla).upgradeTo(address(newImpl)); + + NewbieVilla newbieVilla = NewbieVilla(_newbieVilla); + + // transfer character to newbieVilla + address owner = 0xC8b960D09C0078c18Dcbe7eB9AB9d816BcCa8944; + vm.prank(owner); + IERC721(_web3Entry).safeTransferFrom(owner, _newbieVilla, 10); + assertEq(newbieVilla.getKeeper(10), owner); + + // check storage + assertEq(newbieVilla.web3Entry(), _web3Entry); + assertEq(newbieVilla.getToken(), _token); + assertEq(newbieVilla.hasRole(ADMIN_ROLE, 0x51e2368D60Bc329DBd5834370C1e633bE60C1d6D), true); + + assertEq( + vm.load(address(newbieVilla), bytes32(uint256(7))), + bytes32(uint256(uint160(0x0058be0845952D887D1668B5545de995E12e8783))) + ); + } +} diff --git a/test/helpers/CommonTest.sol b/test/helpers/CommonTest.sol index a9bbbdb3..e48e95b0 100644 --- a/test/helpers/CommonTest.sol +++ b/test/helpers/CommonTest.sol @@ -44,7 +44,7 @@ contract CommonTest is Utils { address public constant admin = address(0x999999999999999999999999999999); address public constant xsyncOperator = address(0xffff4444); - uint256 public constant newbieAdminPrivateKey = 1; + uint256 public constant newbieAdminPrivateKey = 1234567; address public newbieAdmin = vm.addr(newbieAdminPrivateKey); bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE"); diff --git a/test/misc/NewbieVilla.t.sol b/test/misc/NewbieVilla.t.sol index f6d28827..d212c0a1 100644 --- a/test/misc/NewbieVilla.t.sol +++ b/test/misc/NewbieVilla.t.sol @@ -67,31 +67,30 @@ contract NewbieVillaTest is CommonTest { function testNewbieTipCharacter(uint256 amount) public { vm.assume(amount > 0 && amount < 10 ether); - // 1. admin create and transfer web3Entry nft to newbieVilla - web3Entry.createCharacter(makeCharacterData(CHARACTER_HANDLE, newbieAdmin)); - vm.prank(newbieAdmin); - web3Entry.safeTransferFrom(newbieAdmin, address(newbieVilla), FIRST_CHARACTER_ID); + // 1. create and transfer character to newbieVilla + uint256 newbieCharacterId = _createCharacter(CHARACTER_HANDLE, alice); + vm.prank(alice); + web3Entry.safeTransferFrom(alice, address(newbieVilla), newbieCharacterId); - // 2. user create web3Entity nft - vm.prank(bob); - web3Entry.createCharacter(makeCharacterData(CHARACTER_HANDLE2, bob)); + // 2.create character for bob + uint256 bobCharacterId = _createCharacter(CHARACTER_HANDLE2, bob); - // 3. send some token to web3Entry nft in newbieVilla + // 3. send some token to newbieVilla for newbieCharacter vm.prank(alice); - token.send(address(newbieVilla), amount, abi.encode(2, FIRST_CHARACTER_ID)); + token.send(address(newbieVilla), amount, abi.encode(2, newbieCharacterId)); // 4. check balance and state before tip assertEq(token.balanceOf(alice), initialBalance - amount); - assertEq(newbieVilla.balanceOf(FIRST_CHARACTER_ID), amount); + assertEq(newbieVilla.balanceOf(newbieCharacterId), amount); assertEq(token.balanceOf(bob), initialBalance); // 5. tip another character for certain amount vm.prank(alice); - newbieVilla.tipCharacter(FIRST_CHARACTER_ID, SECOND_CHARACTER_ID, amount); + newbieVilla.tipCharacter(newbieCharacterId, bobCharacterId, amount); // 6. check balance and state after tip assertEq(token.balanceOf(alice), initialBalance - amount); - assertEq(newbieVilla.balanceOf(FIRST_CHARACTER_ID), 0); + assertEq(newbieVilla.balanceOf(newbieCharacterId), 0); assertEq(token.balanceOf(bob), initialBalance + amount); } @@ -130,10 +129,10 @@ contract NewbieVillaTest is CommonTest { function testNewbieTipCharacterInsufficientBalanceFail(uint256 amount) public { vm.assume(amount > 0 && amount < 10 ether); - // 1. admin create and transfer web3Entry nft to newbieVilla - uint256 newbieCharacterId = _createCharacter(CHARACTER_HANDLE, newbieAdmin); - vm.prank(newbieAdmin); - web3Entry.safeTransferFrom(newbieAdmin, address(newbieVilla), newbieCharacterId); + // 1. create and transfer web3Entry nft to newbieVilla + uint256 newbieCharacterId = _createCharacter(CHARACTER_HANDLE, alice); + vm.prank(alice); + web3Entry.safeTransferFrom(alice, address(newbieVilla), newbieCharacterId); // 3. send some token to newbieCharacter in newbieVilla contract vm.prank(alice); @@ -146,7 +145,6 @@ contract NewbieVillaTest is CommonTest { // 5. tip another character for certain amount uint256 bobCharacterId = _createCharacter(CHARACTER_HANDLE2, bob); vm.expectRevert(stdError.arithmeticError); - // alice has no permission to tip newbieCharacter // newbieCharacter only has `amount` token in newbieVilla , so it will overflow vm.prank(alice); newbieVilla.tipCharacter(newbieCharacterId, bobCharacterId, amount + 1); @@ -159,110 +157,92 @@ contract NewbieVillaTest is CommonTest { function testNewbieTipCharacterForNote(uint256 amount) public { vm.assume(amount > 0 && amount < 10 ether); - // 1. admin create and transfer web3Entry nft to newbieVilla - web3Entry.createCharacter(makeCharacterData(CHARACTER_HANDLE, newbieAdmin)); - vm.prank(newbieAdmin); - web3Entry.safeTransferFrom(newbieAdmin, address(newbieVilla), FIRST_CHARACTER_ID); + // 1. create and transfer web3Entry nft to newbieVilla + uint256 newbieCharacterId = _createCharacter(CHARACTER_HANDLE, alice); + vm.prank(alice); + web3Entry.safeTransferFrom(alice, address(newbieVilla), newbieCharacterId); - // 2. user create web3Entity nft - vm.prank(bob); - web3Entry.createCharacter(makeCharacterData(CHARACTER_HANDLE2, bob)); + // 2. create character for bob + uint256 bobCharacterId = _createCharacter(CHARACTER_HANDLE2, bob); - // 3. send some token to web3Entry nft in newbieVilla + // 3. send some token to newbieVilla for newbieCharacter vm.prank(alice); - token.send(address(newbieVilla), amount, abi.encode(2, FIRST_CHARACTER_ID)); + token.send(address(newbieVilla), amount, abi.encode(2, newbieCharacterId)); // 4. check balance and state before tip assertEq(token.balanceOf(alice), initialBalance - amount); - assertEq(newbieVilla.balanceOf(FIRST_CHARACTER_ID), amount); + assertEq(newbieVilla.balanceOf(newbieCharacterId), amount); assertEq(token.balanceOf(bob), initialBalance); // 5. tip another character's note for certain amount vm.prank(alice); - newbieVilla.tipCharacterForNote( - FIRST_CHARACTER_ID, - SECOND_CHARACTER_ID, - FIRST_NOTE_ID, - amount - ); + newbieVilla.tipCharacterForNote(newbieCharacterId, bobCharacterId, FIRST_NOTE_ID, amount); // 6. check balance and state after tip assertEq(token.balanceOf(alice), initialBalance - amount); - assertEq(newbieVilla.balanceOf(FIRST_CHARACTER_ID), 0); + assertEq(newbieVilla.balanceOf(newbieCharacterId), 0); assertEq(token.balanceOf(bob), initialBalance + amount); } function testNewbieTipCharacterForNoteNotAuthorizedFail(uint256 amount) public { vm.assume(amount > 0 && amount < 10 ether); - // 1. admin create and transfer web3Entry nft to newbieVilla - web3Entry.createCharacter(makeCharacterData(CHARACTER_HANDLE, newbieAdmin)); - vm.prank(newbieAdmin); - web3Entry.safeTransferFrom(newbieAdmin, address(newbieVilla), FIRST_CHARACTER_ID); - - // 2. user create web3Entity nft - vm.prank(bob); - web3Entry.createCharacter(makeCharacterData(CHARACTER_HANDLE2, bob)); + // 1. create characters + uint256 newbieCharacterId = _createCharacter(CHARACTER_HANDLE, alice); + uint256 bobCharacterId = _createCharacter(CHARACTER_HANDLE2, bob); - // 3. send some token to web3Entry nft in newbieVilla + // 2. alice sends character to newbieVilla vm.prank(alice); - token.send(address(newbieVilla), amount, abi.encode(2, FIRST_CHARACTER_ID)); + web3Entry.safeTransferFrom(alice, address(newbieVilla), newbieCharacterId); - // 4. check balance and state before tip - assertEq(token.balanceOf(alice), initialBalance - amount); - assertEq(newbieVilla.balanceOf(FIRST_CHARACTER_ID), amount); - assertEq(token.balanceOf(bob), initialBalance); + // 3. send some token to newbieVilla for newbieCharacter + vm.prank(alice); + token.send(address(newbieVilla), amount, abi.encode(2, newbieCharacterId)); - // 5. tip another character's note for certain amount - vm.prank(bob); + // case 1: expect revert with no permission vm.expectRevert(abi.encodePacked("NewbieVilla: unauthorized role for tipCharacterForNote")); - newbieVilla.tipCharacterForNote( - FIRST_CHARACTER_ID, - SECOND_CHARACTER_ID, - FIRST_NOTE_ID, - amount - ); + vm.prank(bob); + newbieVilla.tipCharacterForNote(newbieCharacterId, bobCharacterId, 1, amount); - // 6. check balance and state after tip - assertEq(token.balanceOf(alice), initialBalance - amount); - assertEq(newbieVilla.balanceOf(FIRST_CHARACTER_ID), amount); - assertEq(token.balanceOf(bob), initialBalance); + // case 2: expect revert with no permission + vm.expectRevert(abi.encodePacked("NewbieVilla: unauthorized role for tipCharacterForNote")); + vm.prank(newbieAdmin); + newbieVilla.tipCharacterForNote(newbieCharacterId, bobCharacterId, 1, amount); } function testNewbieTipCharacterForNoteInsufficientBalanceFail(uint256 amount) public { vm.assume(amount > 0 && amount < 10 ether); - // 1. admin create and transfer web3Entry nft to newbieVilla - web3Entry.createCharacter(makeCharacterData(CHARACTER_HANDLE, newbieAdmin)); - vm.prank(newbieAdmin); - web3Entry.safeTransferFrom(newbieAdmin, address(newbieVilla), FIRST_CHARACTER_ID); + // 1. create and transfer web3Entry nft to newbieVilla + uint256 newbieCharacterId = _createCharacter(CHARACTER_HANDLE, alice); + vm.prank(alice); + web3Entry.safeTransferFrom(alice, address(newbieVilla), newbieCharacterId); - // 2. user create web3Entity nft - vm.prank(bob); - web3Entry.createCharacter(makeCharacterData(CHARACTER_HANDLE2, bob)); + // 2. create character for bob + uint256 bobCharacterId = _createCharacter(CHARACTER_HANDLE2, bob); - // 3. send some token to web3Entry nft in newbieVilla + // 3. send some token to newbieVilla for newbieCharacter vm.prank(alice); - token.send(address(newbieVilla), amount, abi.encode(2, FIRST_CHARACTER_ID)); + token.send(address(newbieVilla), amount, abi.encode(2, newbieCharacterId)); // 4. check balance and state before tip assertEq(token.balanceOf(alice), initialBalance - amount); - assertEq(newbieVilla.balanceOf(FIRST_CHARACTER_ID), amount); + assertEq(newbieVilla.balanceOf(newbieCharacterId), amount); assertEq(token.balanceOf(bob), initialBalance); // 5. tip another character's note for certain amount vm.prank(alice); vm.expectRevert(stdError.arithmeticError); newbieVilla.tipCharacterForNote( - FIRST_CHARACTER_ID, - SECOND_CHARACTER_ID, + newbieCharacterId, + bobCharacterId, FIRST_NOTE_ID, amount + 1 ); // 6. check balance and state after tip assertEq(token.balanceOf(alice), initialBalance - amount); - assertEq(newbieVilla.balanceOf(FIRST_CHARACTER_ID), amount); + assertEq(newbieVilla.balanceOf(newbieCharacterId), amount); assertEq(token.balanceOf(bob), initialBalance); } @@ -287,27 +267,20 @@ contract NewbieVillaTest is CommonTest { ); } - function testNewbieCreateCharacterFail() public { - // bob has no mint role, so he can't send character to newbieVilla contract - vm.prank(bob); - vm.expectRevert(abi.encodePacked("NewbieVilla: receive unknown character")); - web3Entry.createCharacter(makeCharacterData(CHARACTER_HANDLE, address(newbieVilla))); - } - // transfer character to newbieVilla contract function testTransferNewbieIn() public { - web3Entry.createCharacter(makeCharacterData(CHARACTER_HANDLE, alice)); - vm.prank(alice); - web3Entry.safeTransferFrom(alice, address(newbieVilla), FIRST_CHARACTER_ID); + web3Entry.createCharacter(makeCharacterData(CHARACTER_HANDLE, bob)); + vm.prank(bob); + web3Entry.safeTransferFrom(bob, address(newbieVilla), FIRST_CHARACTER_ID); // check operators address[] memory operators = web3Entry.getOperators(FIRST_CHARACTER_ID); - assertEq(operators[0], alice); + assertEq(operators[0], bob); assertEq(operators[1], xsyncOperator); // check operator permission bitmap - // alice(NewbieVilla admin) has DEFAULT_PERMISSION_BITMAP. + // bob(character's keeper) has DEFAULT_PERMISSION_BITMAP. assertEq( - web3Entry.getOperatorPermissions(FIRST_CHARACTER_ID, alice), + web3Entry.getOperatorPermissions(FIRST_CHARACTER_ID, bob), OP.DEFAULT_PERMISSION_BITMAP ); // xsyncOperator has POST_NOTE_DEFAULT_PERMISSION_BITMAP @@ -315,6 +288,8 @@ contract NewbieVillaTest is CommonTest { web3Entry.getOperatorPermissions(FIRST_CHARACTER_ID, xsyncOperator), OP.POST_NOTE_DEFAULT_PERMISSION_BITMAP ); + // check keeper + assertEq(newbieVilla.getKeeper(FIRST_CHARACTER_ID), bob); } // transfer character to newbieVilla contract with data @@ -347,15 +322,12 @@ contract NewbieVillaTest is CommonTest { web3Entry.getOperatorPermissions(FIRST_CHARACTER_ID, xsyncOperator), OP.POST_NOTE_DEFAULT_PERMISSION_BITMAP ); + // check keeper + assertEq(newbieVilla.getKeeper(FIRST_CHARACTER_ID), alice); } - function testTransferNewbieInFail() public { - web3Entry.createCharacter(makeCharacterData(CHARACTER_HANDLE, bob)); - - vm.expectRevert(abi.encodePacked("NewbieVilla: receive unknown character")); - vm.prank(bob); - web3Entry.safeTransferFrom(bob, address(newbieVilla), FIRST_CHARACTER_ID); - + function testTransferNewbieInFailWithNonCharacterNFT() public { + // transfer non-character nft to newbieVilla contract NFT nft = new NFT(); nft.mint(bob); @@ -396,6 +368,83 @@ contract NewbieVillaTest is CommonTest { assertEq(newbieVilla.balanceOf(characterId), 0); assertEq(web3Entry.ownerOf(characterId), carol); assertEq(token.balanceOf(carol), amount); + // check keeper + assertEq(newbieVilla.getKeeper(characterId), address(0)); + } + + function testWithdrawNewbieOutWithKeeper(uint256 amount) public { + vm.assume(amount > 0 && amount < 10 ether); + address to = carol; + uint256 nonce = 1; + uint256 expires = block.timestamp + 10 minutes; + + uint256 keeperPrivateKey = 1; + address keeper = vm.addr(keeperPrivateKey); + + // 1. create and transfer web3Entry nft to newbieVilla + uint256 characterId = web3Entry.createCharacter( + makeCharacterData(CHARACTER_HANDLE, keeper) + ); + vm.prank(keeper); + web3Entry.safeTransferFrom(keeper, address(newbieVilla), characterId); + + // 2. send some token to web3Entry nft in newbieVilla + vm.prank(alice); + token.send(address(newbieVilla), amount, abi.encode(2, characterId)); + + // 3. withdraw web3Entry nft + bytes32 digest = keccak256( + abi.encodePacked( + "\x19Ethereum Signed Message:\n32", + keccak256(abi.encodePacked(address(newbieVilla), characterId, nonce, expires)) + ) + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(keeperPrivateKey, digest); + // withdraw + vm.prank(to); + newbieVilla.withdraw(to, characterId, nonce, expires, abi.encodePacked(r, s, v)); + + // check state + assertEq(newbieVilla.balanceOf(characterId), 0); + assertEq(web3Entry.ownerOf(characterId), carol); + assertEq(token.balanceOf(carol), amount); + // check keeper + assertEq(newbieVilla.getKeeper(characterId), address(0)); + } + + // newbieVilla admin can't withdraw characters deposited by keeper + function testWithdrawNewbieOutFail(uint256 amount) public { + vm.assume(amount > 0 && amount < 10 ether); + address to = carol; + uint256 nonce = 1; + uint256 expires = block.timestamp + 10 minutes; + + uint256 keeperPrivateKey = newbieAdminPrivateKey + 1; + address keeper = vm.addr(keeperPrivateKey); + + // 1. create and transfer web3Entry nft to newbieVilla + uint256 characterId = web3Entry.createCharacter( + makeCharacterData(CHARACTER_HANDLE, keeper) + ); + vm.prank(keeper); + web3Entry.safeTransferFrom(keeper, address(newbieVilla), characterId); + + // 2. send some token to web3Entry nft in newbieVilla + vm.prank(alice); + token.send(address(newbieVilla), amount, abi.encode(2, characterId)); + + // 3. withdraw web3Entry nft by newbieVilla admin + bytes32 digest = keccak256( + abi.encodePacked( + "\x19Ethereum Signed Message:\n32", + keccak256(abi.encodePacked(address(newbieVilla), characterId, nonce, expires)) + ) + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(newbieAdminPrivateKey, digest); + // withdraw + vm.expectRevert("NewbieVilla: unauthorized withdraw"); + vm.prank(to); + newbieVilla.withdraw(to, characterId, nonce, expires, abi.encodePacked(r, s, v)); } function testTokensReceived(uint256 amount) public { diff --git a/tools/storageLayout/NewbieVilla-storage-layout.txt b/tools/storageLayout/NewbieVilla-storage-layout.txt index f54aebe5..56094a4f 100644 --- a/tools/storageLayout/NewbieVilla-storage-layout.txt +++ b/tools/storageLayout/NewbieVilla-storage-layout.txt @@ -9,3 +9,5 @@ | _token | address | 5 | 0 | 20 | contracts/misc/NewbieVilla.sol:NewbieVilla | | _balances | mapping(uint256 => uint256) | 6 | 0 | 32 | contracts/misc/NewbieVilla.sol:NewbieVilla | | _tips | address | 7 | 0 | 20 | contracts/misc/NewbieVilla.sol:NewbieVilla | +| _keepers | mapping(uint256 => address) | 8 | 0 | 32 | contracts/misc/NewbieVilla.sol:NewbieVilla | +