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 |
+