diff --git a/contracts/TitleEscrow.sol b/contracts/TitleEscrow.sol index 1314d60e..4337a2e2 100644 --- a/contracts/TitleEscrow.sol +++ b/contracts/TitleEscrow.sol @@ -19,6 +19,9 @@ contract TitleEscrow is Initializable, IERC165, TitleEscrowErrors, ITitleEscrow address public override beneficiary; address public override holder; + address public override prevBeneficiary; + address public override prevHolder; + address public override nominee; bool public override active; @@ -157,6 +160,8 @@ contract TitleEscrow is Initializable, IERC165, TitleEscrowErrors, ITitleEscrow if (nominee == _nominee) { revert NomineeAlreadyNominated(); } + prevBeneficiary = address(0); + if (beneficiary == holder) prevHolder = address(0); remark = _remark; _setNominee(_nominee, _remark); @@ -175,6 +180,8 @@ contract TitleEscrow is Initializable, IERC165, TitleEscrowErrors, ITitleEscrow if (!(beneficiary == holder || nominee == _nominee)) { revert InvalidNominee(); } + prevHolder = address(0); + prevBeneficiary = beneficiary; remark = _remark; _setBeneficiary(_nominee, _remark); @@ -193,6 +200,8 @@ contract TitleEscrow is Initializable, IERC165, TitleEscrowErrors, ITitleEscrow if (holder == newHolder) { revert RecipientAlreadyHolder(); } + if (beneficiary == holder) prevBeneficiary = address(0); + prevHolder = holder; remark = _remark; _setHolder(newHolder, _remark); @@ -206,6 +215,80 @@ contract TitleEscrow is Initializable, IERC165, TitleEscrowErrors, ITitleEscrow transferHolder(newHolder, _remark); } + /** + * @dev See {ITitleEscrow-rejectTransferBeneficiary}. + */ + function rejectTransferBeneficiary( + bytes calldata _remark + ) public virtual override whenNotPaused whenActive onlyBeneficiary whenHoldingToken remarkLengthLimit(_remark) { + if (prevBeneficiary == address(0)) { + revert InvalidTransferToZeroAddress(); + } + if (beneficiary == holder) { + revert DualRoleRejectionRequired(); + } + remark = _remark; + address from = beneficiary; + address to = prevBeneficiary; + + _setBeneficiary(to, _remark); + prevBeneficiary = address(0); + emit RejectTransferBeneficiary(from, to, registry, tokenId, _remark); + } + + /** + * @dev See {ITitleEscrow-rejectTransferHolder}. + */ + function rejectTransferHolder( + bytes calldata _remark + ) public virtual override whenNotPaused whenActive onlyHolder whenHoldingToken remarkLengthLimit(_remark) { + if (prevHolder == address(0)) { + revert InvalidTransferToZeroAddress(); + } + if (holder == beneficiary) { + revert DualRoleRejectionRequired(); + } + remark = _remark; + address from = holder; + address to = prevHolder; + + _setHolder(to, _remark); + prevHolder = address(0); + + emit RejectTransferHolder(from, to, registry, tokenId, _remark); + } + + /** + * @dev See {ITitleEscrow-rejectTransferOwners}. + */ + function rejectTransferOwners( + bytes calldata _remark + ) + external + virtual + override + whenNotPaused + whenActive + whenHoldingToken + onlyBeneficiary + onlyHolder + remarkLengthLimit(_remark) + { + if (prevBeneficiary == address(0) || prevHolder == address(0)) { + revert InvalidTransferToZeroAddress(); + } + remark = _remark; + address fromHolder = holder; + address toHolder = prevHolder; + address fromBeneficiary = beneficiary; + address toBeneficiary = prevBeneficiary; + _setBeneficiary(toBeneficiary, _remark); + _setHolder(toHolder, _remark); + prevBeneficiary = address(0); + prevHolder = address(0); + emit RejectTransferOwners(fromBeneficiary, toBeneficiary, fromHolder, toHolder, registry, tokenId, _remark); + } + /** * @dev See {ITitleEscrow-surrender}. */ @@ -225,6 +308,8 @@ contract TitleEscrow is Initializable, IERC165, TitleEscrowErrors, ITitleEscrow _setNominee(address(0), ""); ITradeTrustToken(registry).transferFrom(address(this), registry, tokenId, ""); remark = _remark; + prevBeneficiary = address(0); + prevHolder = address(0); emit Surrender(msg.sender, registry, tokenId, _remark); } diff --git a/contracts/interfaces/ITitleEscrow.sol b/contracts/interfaces/ITitleEscrow.sol index 45c84a69..eb6aeb4d 100644 --- a/contracts/interfaces/ITitleEscrow.sol +++ b/contracts/interfaces/ITitleEscrow.sol @@ -40,6 +40,29 @@ interface ITitleEscrow is IERC721Receiver { ); event Surrender(address indexed surrenderer, address registry, uint256 tokenId, bytes remark); event Shred(address registry, uint256 tokenId, bytes remark); + event RejectTransferOwners( + address indexed fromBeneficiary, + address indexed toBeneficiary, + address indexed fromHolder, + address toHolder, + address registry, + uint256 tokenId, + bytes remark + ); + event RejectTransferBeneficiary( + address indexed fromBeneficiary, + address indexed toBeneficiary, + address registry, + uint256 tokenId, + bytes remark + ); + event RejectTransferHolder( + address indexed fromHolder, + address indexed toHolder, + address registry, + uint256 tokenId, + bytes remark + ); /** * @notice Allows the beneficiary to nominate a new beneficiary @@ -67,10 +90,32 @@ interface ITitleEscrow is IERC721Receiver { */ function transferOwners(address nominee, address newHolder, bytes calldata remark) external; + /** + * @notice Allows the new beneficiary to reject the nomination + * @param _remark The remark for the rejection + */ + function rejectTransferBeneficiary(bytes calldata _remark) external; + + /** + * @notice Allows the new holder to reject the transfer of the holder role + * @param _remark The remark for the rejection + */ + function rejectTransferHolder(bytes calldata _remark) external; + + /** + * @notice Allows the new beneficiary and holder to reject the transfer of both roles + * @param _remark The remark for the rejection + */ + function rejectTransferOwners(bytes calldata _remark) external; + function beneficiary() external view returns (address); function holder() external view returns (address); + function prevBeneficiary() external view returns (address); + + function prevHolder() external view returns (address); + function active() external view returns (bool); function nominee() external view returns (address); diff --git a/contracts/interfaces/TitleEscrowErrors.sol b/contracts/interfaces/TitleEscrowErrors.sol index 052c0bef..31517f43 100644 --- a/contracts/interfaces/TitleEscrowErrors.sol +++ b/contracts/interfaces/TitleEscrowErrors.sol @@ -33,4 +33,6 @@ interface TitleEscrowErrors { error TokenNotSurrendered(); error RemarkLengthExceeded(); + + error DualRoleRejectionRequired(); } diff --git a/src/constants/contract-interfaces.ts b/src/constants/contract-interfaces.ts index 6587e2e7..9290ac3a 100644 --- a/src/constants/contract-interfaces.ts +++ b/src/constants/contract-interfaces.ts @@ -8,8 +8,13 @@ export const contractInterfaces = { "transferBeneficiary(address,bytes)", "transferHolder(address,bytes)", "transferOwners(address,address,bytes)", + "rejectTransferBeneficiary(bytes)", + "rejectTransferHolder(bytes)", + "rejectTransferOwners(bytes)", "beneficiary()", "holder()", + "prevBeneficiary()", + "prevHolder()", "active()", "nominee()", "registry()", diff --git a/test/EndToEnd.test.ts b/test/EndToEnd.test.ts index e96d37d7..b096648f 100644 --- a/test/EndToEnd.test.ts +++ b/test/EndToEnd.test.ts @@ -38,17 +38,34 @@ describe("End to end", () => { let fakeTokenSymbol: string; let tokenRegistry: TradeTrustToken; let titleEscrow: TitleEscrow; + + let prevBeneficiary: SignerWithAddress; let beneficiary: SignerWithAddress; + let newBeneficiary: SignerWithAddress; + + let prevHolder: SignerWithAddress; let holder: SignerWithAddress; - let holder1: SignerWithAddress; - let nominee: string; - let nominee1: SignerWithAddress; - let nominee2: SignerWithAddress; + let newHolder: SignerWithAddress; + + let nominee: SignerWithAddress; + let tokenId: string; let registryAdmin: SignerWithAddress; let minter: SignerWithAddress; let restorer: SignerWithAddress; let accepter: SignerWithAddress; + + let testNominee: SignerWithAddress; + + let testHolder1: SignerWithAddress; + let testHolder2: SignerWithAddress; + let testHolder3: SignerWithAddress; + let testHolder4: SignerWithAddress; + + let testBeneficiary1: SignerWithAddress; + let testBeneficiary2: SignerWithAddress; + let testBeneficiary3: SignerWithAddress; + const exceededLengthRemark = ethers.utils.hexlify(ethers.utils.randomBytes(121)); let deployFixturesRunner: () => Promise<[TradeTrustTokenStandard, TitleEscrowFactory, TDocDeployer]>; @@ -56,8 +73,20 @@ describe("End to end", () => { before(async () => { users = await getTestUsers(); deployer = users.carrier; - nominee = defaultAddress.Zero; - [, , , , nominee1, nominee2, holder1] = users.others; + [ + , + , + , + , + testHolder1, + testHolder2, + testHolder3, + testHolder4, + testBeneficiary1, + testBeneficiary2, + testBeneficiary3, + testNominee, + ] = users.others; tokenId = faker.datatype.hexaDecimal(64); fakeTokenName = "The Great Shipping Co."; fakeTokenSymbol = "GSC"; @@ -231,6 +260,16 @@ describe("End to end", () => { tokenRegistry.connect(accepter).burn(tokenId, txnHexRemarks.burnRemark) ).to.be.revertedWithCustomError(titleEscrow, "TokenNotSurrendered"); }); + it("should revert when trying rejectTranferBeneficiary", async () => { + await expect( + titleEscrow.connect(beneficiary).rejectTransferBeneficiary(txnHexRemarks.rejectTransferRemark) + ).to.be.revertedWithCustomError(titleEscrow, "InvalidTransferToZeroAddress"); + }); + it("should revert when trying rejectTranferHolder", async () => { + await expect( + titleEscrow.connect(holder).rejectTransferHolder(txnHexRemarks.rejectTransferRemark) + ).to.be.revertedWithCustomError(titleEscrow, "InvalidTransferToZeroAddress"); + }); }); describe("Paused", () => { it("should not allow pause when called by non-admin", async () => { @@ -320,49 +359,66 @@ describe("End to end", () => { expect(await titleEscrow.holder()).to.equal(holder.address); expect(await titleEscrow.nominee()).to.equal(ethers.constants.AddressZero); expect(await titleEscrow.active()).to.be.true; + expect(await titleEscrow.prevBeneficiary()).to.equal(defaultAddress.Zero); + expect(await titleEscrow.prevHolder()).to.equal(defaultAddress.Zero); }); describe("Transfer Beneficiary", () => { + // eslint-disable-next-line no-undef + before(async () => { + nominee = testNominee; + }); describe("When Holder and Beneficiary are different", () => { it("1: should allow Beneficiary to nominate ", async () => { - await expect(titleEscrow.connect(beneficiary).nominate(nominee1.address, txnHexRemarks.nominateRemark)) + await expect(titleEscrow.connect(beneficiary).nominate(nominee.address, txnHexRemarks.nominateRemark)) .to.emit(titleEscrow, "Nomination") - .withArgs(nominee, nominee1.address, tokenRegistry.address, tokenId, txnHexRemarks.nominateRemark); + .withArgs( + defaultAddress.Zero, + nominee.address, + tokenRegistry.address, + tokenId, + txnHexRemarks.nominateRemark + ); expect(await titleEscrow.remark()).to.equal(txnHexRemarks.nominateRemark); }); it("2: should allow holder to transfer beneficiary", async () => { - const newBeneficiary = nominee1; + newBeneficiary = nominee; + prevBeneficiary = beneficiary; await expect( - titleEscrow - .connect(holder) - .transferBeneficiary(newBeneficiary.address, txnHexRemarks.beneficiaryTransferRemark) + titleEscrow.connect(holder).transferBeneficiary(nominee.address, txnHexRemarks.beneficiaryTransferRemark) ) .to.emit(titleEscrow, "BeneficiaryTransfer") .withArgs( beneficiary.address, - newBeneficiary.address, + nominee.address, tokenRegistry.address, tokenId, txnHexRemarks.beneficiaryTransferRemark ); expect(await titleEscrow.beneficiary()).to.equal(newBeneficiary.address); + expect(await titleEscrow.prevBeneficiary()).to.equal(prevBeneficiary.address); const remark = await titleEscrow.remark(); expect(remark).to.equal(txnHexRemarks.beneficiaryTransferRemark); expect(hexToString(remark)).to.equal(remarkString.beneficiaryTransferRemark); }); }); describe("When Holder and Beneficiary are same", () => { - // eslint-disable-next-line no-undef + // eslint-disable-next-line no-undef before(async () => { // current beneficiary is nominee1 // current holder is holder // transfer beneficiary to holder - await titleEscrow.connect(nominee1).nominate(holder.address, txnHexRemarks.nominateRemark); + prevBeneficiary = beneficiary; + beneficiary = nominee; + newBeneficiary = testBeneficiary1; + + // transaction to make beneficiary and holder same + await titleEscrow.connect(beneficiary).nominate(holder.address, txnHexRemarks.nominateRemark); await titleEscrow .connect(holder) .transferBeneficiary(holder.address, txnHexRemarks.beneficiaryTransferRemark); + beneficiary = holder; }); it("should allow holder to transfer beneficiary", async () => { - const newBeneficiary = nominee2; await expect( titleEscrow .connect(holder) @@ -370,7 +426,7 @@ describe("End to end", () => { ) .to.emit(titleEscrow, "BeneficiaryTransfer") .withArgs( - holder.address, + beneficiary.address, newBeneficiary.address, tokenRegistry.address, tokenId, @@ -396,7 +452,7 @@ describe("End to end", () => { }); it("should allow transfer to the new holder", async () => { - const newHolder = holder1; + newHolder = testHolder1; await expect(titleEscrow.connect(holder).transferHolder(newHolder.address, txnHexRemarks.holderTransferRemark)) .to.emit(titleEscrow, "HolderTransfer") .withArgs( @@ -407,37 +463,35 @@ describe("End to end", () => { txnHexRemarks.holderTransferRemark ); expect(await titleEscrow.holder()).to.equal(newHolder.address); + expect(await titleEscrow.prevHolder()).to.equal(holder.address); const remark = await titleEscrow.remark(); expect(remark).to.equal(txnHexRemarks.holderTransferRemark); expect(hexToString(remark)).to.equal(remarkString.holderTransferRemark); }); }); describe("Transfer Owners", () => { - let currHolder: SignerWithAddress; - let newHolder: SignerWithAddress; - let newBeneficiary: SignerWithAddress; // eslint-disable-next-line no-undef before(async () => { - currHolder = holder1; - newHolder = holder; - newBeneficiary = holder; - const prevBeneficiary = nominee2; - await titleEscrow.connect(prevBeneficiary).nominate(currHolder.address, txnHexRemarks.nominateRemark); - await titleEscrow - .connect(currHolder) - .transferBeneficiary(currHolder.address, txnHexRemarks.beneficiaryTransferRemark); + holder = newHolder; + newHolder = testHolder2; + beneficiary = newBeneficiary; + newBeneficiary = newHolder; + await titleEscrow.connect(beneficiary).nominate(holder.address, txnHexRemarks.nominateRemark); + await titleEscrow.connect(holder).transferBeneficiary(holder.address, txnHexRemarks.beneficiaryTransferRemark); + prevBeneficiary = beneficiary; + beneficiary = holder; }); it("should not allow beneficiary transfer to zero address", async () => { await expect( titleEscrow - .connect(currHolder) + .connect(holder) .transferOwners(defaultAddress.Zero, newHolder.address, txnHexRemarks.transferOwnersRemark) ).to.be.revertedWithCustomError(titleEscrow, "InvalidTransferToZeroAddress"); }); it("should not allow holder transfer to zero address", async () => { await expect( titleEscrow - .connect(currHolder) + .connect(holder) .transferOwners(newBeneficiary.address, defaultAddress.Zero, txnHexRemarks.transferOwnersRemark) ).to.be.revertedWithCustomError(titleEscrow, "InvalidTransferToZeroAddress"); }); @@ -445,12 +499,12 @@ describe("End to end", () => { // new holder and new beneficiary are same await expect( titleEscrow - .connect(currHolder) + .connect(holder) .transferOwners(newBeneficiary.address, newHolder.address, txnHexRemarks.transferOwnersRemark) ) .to.emit(titleEscrow, "BeneficiaryTransfer") .withArgs( - currHolder.address, + holder.address, newBeneficiary.address, tokenRegistry.address, tokenId, @@ -458,7 +512,7 @@ describe("End to end", () => { ) .and.to.emit(titleEscrow, "HolderTransfer") .withArgs( - currHolder.address, + holder.address, newHolder.address, tokenRegistry.address, tokenId, @@ -467,39 +521,158 @@ describe("End to end", () => { expect(await titleEscrow.holder()).to.equal(newHolder.address); expect(await titleEscrow.beneficiary()).to.equal(newBeneficiary.address); + expect(await titleEscrow.prevHolder()).to.equal(holder.address); + expect(await titleEscrow.prevBeneficiary()).to.equal(holder.address); const remark = await titleEscrow.remark(); expect(remark).to.equal(txnHexRemarks.transferOwnersRemark); expect(hexToString(remark)).to.equal(remarkString.transferOwnersRemark); }); it("should allow transfer to different new holder and beneficiary", async () => { + prevHolder = holder; + holder = newHolder; + newHolder = testHolder3; + prevBeneficiary = holder; + beneficiary = newBeneficiary; + newBeneficiary = testBeneficiary2; // new holder and new beneficiary are different await expect( titleEscrow - .connect(newHolder) - .transferOwners(nominee2.address, holder1.address, txnHexRemarks.transferOwnersRemark) + .connect(holder) + .transferOwners(newBeneficiary.address, newHolder.address, txnHexRemarks.transferOwnersRemark) ) .to.emit(titleEscrow, "BeneficiaryTransfer") .withArgs( - newHolder.address, - nominee2.address, + beneficiary.address, + newBeneficiary.address, tokenRegistry.address, tokenId, txnHexRemarks.transferOwnersRemark ) .and.to.emit(titleEscrow, "HolderTransfer") .withArgs( + holder.address, newHolder.address, - holder1.address, tokenRegistry.address, tokenId, txnHexRemarks.transferOwnersRemark ); - expect(await titleEscrow.beneficiary()).to.equal(nominee2.address); - expect(await titleEscrow.holder()).to.equal(holder1.address); + expect(await titleEscrow.beneficiary()).to.equal(newBeneficiary.address); + expect(await titleEscrow.holder()).to.equal(newHolder.address); + expect(await titleEscrow.prevHolder()).to.equal(holder.address); + expect(await titleEscrow.prevBeneficiary()).to.equal(beneficiary.address); }); }); + describe("Reject Transfer Beneficiary", () => { + // eslint-disable-next-line no-undef + before(async () => { + prevBeneficiary = beneficiary; + beneficiary = newBeneficiary; + }); + it("should not allow reject transfer beneficiary if the caller is not beneficiary", async () => { + await expect( + titleEscrow.connect(holder).rejectTransferBeneficiary(txnHexRemarks.rejectTransferRemark) + ).to.be.revertedWithCustomError(titleEscrow, "CallerNotBeneficiary"); + }); + it("should allow beneficiary to reject transfer beneficiary", async () => { + await expect(titleEscrow.connect(beneficiary).rejectTransferBeneficiary(txnHexRemarks.rejectTransferRemark)) + .to.emit(titleEscrow, "RejectTransferBeneficiary") + .withArgs( + beneficiary.address, + prevBeneficiary.address, + tokenRegistry.address, + tokenId, + txnHexRemarks.rejectTransferRemark + ); + expect(await titleEscrow.beneficiary()).to.equal(prevBeneficiary.address); + expect(await titleEscrow.prevBeneficiary()).to.equal(defaultAddress.Zero); + expect(await titleEscrow.remark()).to.equal(txnHexRemarks.rejectTransferRemark); + }); + }); + describe("Reject Transfer Holder", () => { + // eslint-disable-next-line no-undef + before(async () => { + prevHolder = holder; + holder = newHolder; + }); + it("should not allow reject transfer holder if the caller is not holder", async () => { + await expect( + titleEscrow.connect(beneficiary).rejectTransferHolder(txnHexRemarks.rejectTransferRemark) + ).to.be.revertedWithCustomError(titleEscrow, "CallerNotHolder"); + }); + it("should allow holder to reject transfer holder", async () => { + await expect(titleEscrow.connect(holder).rejectTransferHolder(txnHexRemarks.rejectTransferRemark)) + .to.emit(titleEscrow, "RejectTransferHolder") + .withArgs( + holder.address, + prevHolder.address, + tokenRegistry.address, + tokenId, + txnHexRemarks.rejectTransferRemark + ); + expect(await titleEscrow.holder()).to.equal(prevHolder.address); + expect(await titleEscrow.prevHolder()).to.equal(defaultAddress.Zero); + expect(await titleEscrow.remark()).to.equal(txnHexRemarks.rejectTransferRemark); + }); + }); + + describe("Reject Transfer Owners", () => { + // eslint-disable-next-line no-undef + before(async () => { + holder = prevHolder; + beneficiary = prevBeneficiary; + newHolder = testHolder3; + newBeneficiary = newHolder; // transfer to same holder and beneficiary + await titleEscrow + .connect(holder) + .transferOwners(newBeneficiary.address, newHolder.address, txnHexRemarks.transferOwnersRemark); + prevBeneficiary = beneficiary; + beneficiary = newBeneficiary; + prevHolder = holder; + holder = newHolder; + }); + + it("should not allow reject transfer owners if the caller is not holder", async () => { + await expect( + titleEscrow.connect(prevHolder).rejectTransferOwners(txnHexRemarks.rejectTransferRemark) + ).to.be.revertedWithCustomError(titleEscrow, "CallerNotBeneficiary"); + }); + it("should not allow holder to reject only holder transfer", async () => { + const tx = titleEscrow.connect(holder).rejectTransferHolder(txnHexRemarks.rejectTransferRemark); + + await expect(tx).to.be.revertedWithCustomError(titleEscrow, "DualRoleRejectionRequired"); + }); + it("should not allow beneficiary to reject only beneficiary transfer", async () => { + const tx = titleEscrow.connect(holder).rejectTransferBeneficiary(txnHexRemarks.rejectTransferRemark); + + await expect(tx).to.be.revertedWithCustomError(titleEscrow, "DualRoleRejectionRequired"); + }); + it("should allow holder to reject transfer owners", async () => { + await expect(titleEscrow.connect(holder).rejectTransferOwners(txnHexRemarks.rejectTransferRemark)) + .to.emit(titleEscrow, "RejectTransferOwners") + .withArgs( + beneficiary.address, + prevBeneficiary.address, + holder.address, + prevHolder.address, + tokenRegistry.address, + tokenId, + txnHexRemarks.rejectTransferRemark + ); + expect(await titleEscrow.holder()).to.equal(prevHolder.address); + expect(await titleEscrow.beneficiary()).to.equal(prevBeneficiary.address); + expect(await titleEscrow.prevHolder()).to.equal(defaultAddress.Zero); + expect(await titleEscrow.prevBeneficiary()).to.equal(defaultAddress.Zero); + expect(await titleEscrow.remark()).to.equal(txnHexRemarks.rejectTransferRemark); + }); + }); + describe("Paused", () => { + // eslint-disable-next-line no-undef + before(async () => { + newBeneficiary = testBeneficiary3; + newHolder = testHolder4; + }); it("Paused: should emit correct event with args when paused", async () => { const tx = tokenRegistry.connect(registryAdmin).pause(txnHexRemarks.pauseRemark); @@ -511,19 +684,21 @@ describe("End to end", () => { it("should not allow nomination when paused", async () => { // eslint-disable-next-line no-undef await expect( - titleEscrow.connect(beneficiary).nominate(nominee1.address, txnHexRemarks.nominateRemark) + titleEscrow.connect(beneficiary).nominate(newBeneficiary.address, txnHexRemarks.nominateRemark) ).to.be.revertedWithCustomError(titleEscrow, "RegistryContractPaused"); }); it("should not allow transfer beneficiary when paused", async () => { await expect( - titleEscrow.connect(holder).transferBeneficiary(nominee1.address, txnHexRemarks.beneficiaryTransferRemark) + titleEscrow + .connect(holder) + .transferBeneficiary(newBeneficiary.address, txnHexRemarks.beneficiaryTransferRemark) ).to.be.revertedWithCustomError(titleEscrow, "RegistryContractPaused"); }); it("should not allow transfer holder when paused", async () => { await expect( - titleEscrow.connect(holder).transferHolder(nominee1.address, txnHexRemarks.holderTransferRemark) + titleEscrow.connect(holder).transferHolder(newHolder.address, txnHexRemarks.holderTransferRemark) ).to.be.revertedWithCustomError(titleEscrow, "RegistryContractPaused"); }); @@ -531,7 +706,7 @@ describe("End to end", () => { await expect( titleEscrow .connect(holder) - .transferOwners(nominee1.address, holder1.address, txnHexRemarks.transferOwnersRemark) + .transferOwners(newBeneficiary.address, newHolder.address, txnHexRemarks.transferOwnersRemark) ).to.be.revertedWithCustomError(titleEscrow, "RegistryContractPaused"); }); it("should not allow surrender when paused", async () => { @@ -539,6 +714,21 @@ describe("End to end", () => { titleEscrow.connect(holder).surrender(txnHexRemarks.surrenderRemark) ).to.be.revertedWithCustomError(titleEscrow, "RegistryContractPaused"); }); + it("should not allow reject transfer beneficiary when paused", async () => { + await expect( + titleEscrow.connect(beneficiary).rejectTransferBeneficiary(txnHexRemarks.rejectTransferRemark) + ).to.be.revertedWithCustomError(titleEscrow, "RegistryContractPaused"); + }); + it("should not allow reject transfer holder when paused", async () => { + await expect( + titleEscrow.connect(holder).rejectTransferHolder(txnHexRemarks.rejectTransferRemark) + ).to.be.revertedWithCustomError(titleEscrow, "RegistryContractPaused"); + }); + it("should not allow reject owner transfer when paused", async () => { + await expect( + titleEscrow.connect(holder).rejectTransferOwners(txnHexRemarks.rejectTransferRemark) + ).to.be.revertedWithCustomError(titleEscrow, "RegistryContractPaused"); + }); it("UnPaused: should emit correct event with args when unpaused", async () => { const tx = tokenRegistry.connect(registryAdmin).unpause(txnHexRemarks.unPauseRemark); await expect(tx) @@ -547,16 +737,23 @@ describe("End to end", () => { }); }); describe("Surrender", () => { + // eslint-disable-next-line no-undef + before(async () => { + holder = prevHolder; + beneficiary = prevBeneficiary; + await titleEscrow.connect(holder).transferHolder(testHolder4.address, txnHexRemarks.holderTransferRemark); + holder = testHolder4; + }); describe("When Holder and Beneficiary are not same", () => { it("should revert surrender if caller is not beneficiary", async () => { await expect( - titleEscrow.connect(holder1).surrender(txnHexRemarks.surrenderRemark) + titleEscrow.connect(holder).surrender(txnHexRemarks.surrenderRemark) ).to.be.revertedWithCustomError(titleEscrow, "CallerNotBeneficiary"); }); // current beneficiary is nominee2 it("should revert surrender if the caller is not holder", async () => { await expect( - titleEscrow.connect(nominee2).surrender(txnHexRemarks.surrenderRemark) + titleEscrow.connect(beneficiary).surrender(txnHexRemarks.surrenderRemark) ).to.be.revertedWithCustomError(titleEscrow, "CallerNotHolder"); }); }); @@ -564,13 +761,10 @@ describe("End to end", () => { // eslint-disable-next-line no-undef before(async () => { // setting both holder and beneficiary to the same address - const currBeneficiary = nominee2; - const newBeneficiary = holder; - await titleEscrow.connect(currBeneficiary).nominate(newBeneficiary.address, txnHexRemarks.nominateRemark); await titleEscrow - .connect(holder1) - .transferBeneficiary(newBeneficiary.address, txnHexRemarks.beneficiaryTransferRemark); - await titleEscrow.connect(holder1).transferHolder(holder.address, txnHexRemarks.holderTransferRemark); + .connect(holder) + .transferHolder(beneficiary.address, txnHexRemarks.beneficiaryTransferRemark); + holder = beneficiary; }); it("should allow surrendering if the contract holds the token", async () => { await expect(titleEscrow.connect(holder).surrender(txnHexRemarks.surrenderRemark)) @@ -578,6 +772,8 @@ describe("End to end", () => { .withArgs(holder.address, tokenRegistry.address, tokenId, txnHexRemarks.surrenderRemark); // token id owner to be token registry after surrender expect(await tokenRegistry.ownerOf(tokenId)).to.equal(tokenRegistry.address); + expect(await titleEscrow.prevHolder()).to.equal(defaultAddress.Zero); + expect(await titleEscrow.prevBeneficiary()).to.equal(defaultAddress.Zero); const remark = await titleEscrow.remark(); expect(remark).to.equal(txnHexRemarks.surrenderRemark); expect(hexToString(remark)).to.equal(remarkString.surrenderRemark); @@ -587,24 +783,24 @@ describe("End to end", () => { describe("After Surrender", () => { it("should not allow nomination", async () => { await expect( - titleEscrow.connect(holder).nominate(nominee1.address, txnHexRemarks.nominateRemark) + titleEscrow.connect(holder).nominate(nominee.address, txnHexRemarks.nominateRemark) ).to.be.revertedWithCustomError(titleEscrow, "TitleEscrowNotHoldingToken"); }); it("should not allow transfer beneficiary", async () => { await expect( - titleEscrow.connect(holder).transferBeneficiary(nominee1.address, txnHexRemarks.beneficiaryTransferRemark) + titleEscrow.connect(holder).transferBeneficiary(nominee.address, txnHexRemarks.beneficiaryTransferRemark) ).to.be.revertedWithCustomError(titleEscrow, "TitleEscrowNotHoldingToken"); }); it("should not allow transfer holder", async () => { await expect( - titleEscrow.connect(holder).transferHolder(nominee1.address, txnHexRemarks.holderTransferRemark) + titleEscrow.connect(holder).transferHolder(testHolder4.address, txnHexRemarks.holderTransferRemark) ).to.be.revertedWithCustomError(titleEscrow, "TitleEscrowNotHoldingToken"); }); it("should not allow transfer owners", async () => { await expect( titleEscrow .connect(holder) - .transferOwners(nominee1.address, holder1.address, txnHexRemarks.transferOwnersRemark) + .transferOwners(nominee.address, testHolder4.address, txnHexRemarks.transferOwnersRemark) ).to.be.revertedWithCustomError(titleEscrow, "TitleEscrowNotHoldingToken"); }); it("should not allow surrender", async () => { @@ -612,6 +808,21 @@ describe("End to end", () => { titleEscrow.connect(holder).surrender(txnHexRemarks.surrenderRemark) ).to.be.revertedWithCustomError(titleEscrow, "TitleEscrowNotHoldingToken"); }); + it("should not allow reject transfer beneficiary", async () => { + await expect( + titleEscrow.connect(beneficiary).rejectTransferBeneficiary(txnHexRemarks.rejectTransferRemark) + ).to.be.revertedWithCustomError(titleEscrow, "TitleEscrowNotHoldingToken"); + }); + it("should not allow reject transfer holder", async () => { + await expect( + titleEscrow.connect(holder).rejectTransferHolder(txnHexRemarks.rejectTransferRemark) + ).to.be.revertedWithCustomError(titleEscrow, "TitleEscrowNotHoldingToken"); + }); + it("should not allow reject owner transfer", async () => { + await expect( + titleEscrow.connect(holder).rejectTransferOwners(txnHexRemarks.rejectTransferRemark) + ).to.be.revertedWithCustomError(titleEscrow, "TitleEscrowNotHoldingToken"); + }); }); describe("Restore", () => { it("should not allow restore if the caller is holder", async () => { @@ -633,7 +844,7 @@ describe("End to end", () => { await expect(tokenRegistry.connect(restorer).restore(tokenId, txnHexRemarks.restorerRemark)) .to.emit(titleEscrow, "TokenReceived") .withArgs( - holder.address, + beneficiary.address, holder.address, false, tokenRegistry.address, @@ -657,6 +868,10 @@ describe("End to end", () => { it("owner should be set back to titleEscrow", async () => { expect(await tokenRegistry.ownerOf(tokenId)).to.equal(titleEscrow.address); }); + it("should have zero address as prevBeneficiary and prevHolder", async () => { + expect(await titleEscrow.prevBeneficiary()).to.equal(defaultAddress.Zero); + expect(await titleEscrow.prevHolder()).to.equal(defaultAddress.Zero); + }); }); describe("Burn", () => { // eslint-disable-next-line no-undef @@ -703,24 +918,39 @@ describe("End to end", () => { }); it("should not allow nomination", async () => { await expect( - titleEscrow.connect(beneficiary).nominate(nominee1.address, txnHexRemarks.nominateRemark) + titleEscrow.connect(beneficiary).nominate(nominee.address, txnHexRemarks.nominateRemark) ).to.be.revertedWithCustomError(titleEscrow, "InactiveTitleEscrow"); }); it("should not allow transfer beneficiary", async () => { await expect( - titleEscrow.connect(holder).transferBeneficiary(nominee1.address, txnHexRemarks.beneficiaryTransferRemark) + titleEscrow.connect(holder).transferBeneficiary(nominee.address, txnHexRemarks.beneficiaryTransferRemark) ).to.be.revertedWithCustomError(titleEscrow, "InactiveTitleEscrow"); }); it("should not allow transfer holder", async () => { await expect( - titleEscrow.connect(holder).transferHolder(nominee1.address, txnHexRemarks.holderTransferRemark) + titleEscrow.connect(holder).transferHolder(testHolder4.address, txnHexRemarks.holderTransferRemark) ).to.be.revertedWithCustomError(titleEscrow, "InactiveTitleEscrow"); }); it("should not allow transfer owners", async () => { await expect( titleEscrow .connect(holder) - .transferOwners(nominee1.address, holder1.address, txnHexRemarks.transferOwnersRemark) + .transferOwners(nominee.address, testHolder4.address, txnHexRemarks.transferOwnersRemark) + ).to.be.revertedWithCustomError(titleEscrow, "InactiveTitleEscrow"); + }); + it("should not allow reject transfer beneficiary", async () => { + await expect( + titleEscrow.connect(beneficiary).rejectTransferBeneficiary(txnHexRemarks.rejectTransferRemark) + ).to.be.revertedWithCustomError(titleEscrow, "InactiveTitleEscrow"); + }); + it("should not allow reject transfer holder", async () => { + await expect( + titleEscrow.connect(holder).rejectTransferHolder(txnHexRemarks.rejectTransferRemark) + ).to.be.revertedWithCustomError(titleEscrow, "InactiveTitleEscrow"); + }); + it("should not allow reject owner transfer", async () => { + await expect( + titleEscrow.connect(holder).rejectTransferOwners(txnHexRemarks.rejectTransferRemark) ).to.be.revertedWithCustomError(titleEscrow, "InactiveTitleEscrow"); }); it("should not allow surrender", async () => { diff --git a/test/TitleEscrow.test.ts b/test/TitleEscrow.test.ts index 675a879e..304dbbef 100644 --- a/test/TitleEscrow.test.ts +++ b/test/TitleEscrow.test.ts @@ -458,7 +458,7 @@ describe("Title Escrow", async () => { }); }); - describe("Operational Behaviours", () => { + describe("Operational Behaviors", () => { let registryContract: TradeTrustToken; let titleEscrowOwnerContract: TitleEscrow; @@ -583,6 +583,30 @@ describe("Title Escrow", async () => { await expect(tx).to.be.revertedWithCustomError(titleEscrowAsBeneficiary, "TitleEscrowNotHoldingToken"); }); + it("should set prevBeneficiary to zero address upon nomination", async () => { + const [owner, newBeneficiary] = users.others; + // setting holder and beneficiary to same address + await titleEscrowOwnerContract + .connect(users.holder) + .transferHolder(users.beneficiary.address, txnHexRemarks.transferOwnersRemark); + + // to set valid prev owners + await titleEscrowOwnerContract + .connect(users.beneficiary) + .transferOwners(owner.address, owner.address, txnHexRemarks.transferOwnersRemark); + + await titleEscrowOwnerContract + .connect(owner) + .transferBeneficiary(newBeneficiary.address, txnHexRemarks.beneficiaryTransferRemark); + + expect(await titleEscrowOwnerContract.prevBeneficiary()).to.equal(owner.address); + + await titleEscrowOwnerContract + .connect(newBeneficiary) + .nominate(beneficiaryNominee.address, txnHexRemarks.nominateRemark); + expect(await titleEscrowOwnerContract.prevBeneficiary()).to.equal(defaultAddress.Zero); + }); + it("should emit Nomination event", async () => { const tx = titleEscrowOwnerContract .connect(users.beneficiary) @@ -631,20 +655,28 @@ describe("Title Escrow", async () => { it("should allow a beneficiary who is also a holder to transfer to a non-nominated beneficiary", async () => { const fakeTokenId = faker.datatype.hexaDecimal(64); - const [targetNonBeneficiaryNominee] = users.others; + const [targetNewOwner, targetNonBeneficiaryNominee] = users.others; await registryContract .connect(users.carrier) .mint(users.beneficiary.address, users.beneficiary.address, fakeTokenId, txnHexRemarks.mintRemark); titleEscrowOwnerContract = await getTitleEscrowContract(registryContract, fakeTokenId); const initialBeneficiaryNominee = await titleEscrowOwnerContract.nominee(); + + // to set valid prevBeneficiary and prevHolder await titleEscrowOwnerContract .connect(users.beneficiary) + .transferOwners(targetNewOwner.address, targetNewOwner.address, txnHexRemarks.transferOwnersRemark); + + await titleEscrowOwnerContract + .connect(targetNewOwner) .transferBeneficiary(targetNonBeneficiaryNominee.address, txnHexRemarks.beneficiaryTransferRemark); - const currentBeneficiary = await titleEscrowOwnerContract.beneficiary(); expect(initialBeneficiaryNominee).to.equal(defaultAddress.Zero); - expect(currentBeneficiary).to.equal(targetNonBeneficiaryNominee.address); + expect(await titleEscrowOwnerContract.beneficiary()).to.equal(targetNonBeneficiaryNominee.address); + expect(await titleEscrowOwnerContract.holder()).to.equal(targetNewOwner.address); + expect(await titleEscrowOwnerContract.prevHolder()).to.equal(defaultAddress.Zero); + expect(await titleEscrowOwnerContract.prevBeneficiary()).to.equal(targetNewOwner.address); }); it("should not allow non-holder to transfer to a nominated beneficiary", async () => { @@ -735,24 +767,31 @@ describe("Title Escrow", async () => { const res = await titleEscrowOwnerContract.holder(); expect(res).to.equal(targetNewHolder.address); + expect(await titleEscrowOwnerContract.prevHolder()).to.equal(users.holder.address); }); it("should allow a holder who is also a beneficiary to transfer holder", async () => { const fakeTokenId = faker.datatype.hexaDecimal(64); - const [targetNonNominatedHolder] = users.others; + const [targetNewOwner, targetNonNominatedHolder] = users.others; await registryContract .connect(users.carrier) .mint(users.beneficiary.address, users.beneficiary.address, fakeTokenId, txnHexRemarks.mintRemark); titleEscrowOwnerContract = await getTitleEscrowContract(registryContract, fakeTokenId); - const initialBeneficiaryNominee = await titleEscrowOwnerContract.nominee(); + // to set valid prevBeneficiary and prevHolder await titleEscrowOwnerContract .connect(users.beneficiary) - .transferHolder(targetNonNominatedHolder.address, txnHexRemarks.holderTransferRemark); - const currentHolder = await titleEscrowOwnerContract.holder(); + .transferOwners(targetNewOwner.address, targetNewOwner.address, txnHexRemarks.transferOwnersRemark); + await titleEscrowOwnerContract + .connect(targetNewOwner) + .transferHolder(targetNonNominatedHolder.address, txnHexRemarks.holderTransferRemark); + const initialBeneficiaryNominee = await titleEscrowOwnerContract.nominee(); expect(initialBeneficiaryNominee).to.equal(defaultAddress.Zero); - expect(currentHolder).to.equal(targetNonNominatedHolder.address); + expect(await titleEscrowOwnerContract.beneficiary()).to.equal(targetNewOwner.address); + expect(await titleEscrowOwnerContract.holder()).to.equal(targetNonNominatedHolder.address); + expect(await titleEscrowOwnerContract.prevHolder()).to.equal(targetNewOwner.address); + expect(await titleEscrowOwnerContract.prevBeneficiary()).to.equal(defaultAddress.Zero); }); it("should not allow a non-holder to transfer to a nominated holder", async () => { @@ -808,17 +847,22 @@ describe("Title Escrow", async () => { .nominate(beneficiaryNominee.address, txnHexRemarks.nominateRemark); }); - it("should call transferBeneficiary and transferHolder interally", async () => { + it("should call transferBeneficiary and transferHolder internally", async () => { await titleEscrowOwnerContract .connect(users.holder) - .transferOwners(beneficiaryNominee.address, holderNominee.address, txnHexRemarks.holderTransferRemark); - const [currentBeneficiary, currentHolder] = await Promise.all([ + .transferOwners(beneficiaryNominee.address, holderNominee.address, txnHexRemarks.transferOwnersRemark); + const [currentBeneficiary, currentHolder, prevBeneficiary, prevHolder] = await Promise.all([ titleEscrowOwnerContract.beneficiary(), titleEscrowOwnerContract.holder(), + titleEscrowOwnerContract.prevBeneficiary(), + titleEscrowOwnerContract.prevHolder(), ]); expect(currentBeneficiary).to.equal(beneficiaryNominee.address); expect(currentHolder).to.equal(holderNominee.address); + expect(prevBeneficiary).to.equal(users.beneficiary.address); + expect(prevHolder).to.equal(users.holder.address); + expect(await titleEscrowOwnerContract.remark()).to.equal(txnHexRemarks.transferOwnersRemark); }); it("should revert when caller is not holder", async () => { @@ -856,13 +900,453 @@ describe("Title Escrow", async () => { }); }); + describe("Beneficiary and Holder Rejection", () => { + beforeEach(async () => { + await registryContract + .connect(users.carrier) + .mint(users.beneficiary.address, users.holder.address, tokenId, txnHexRemarks.mintRemark); + titleEscrowOwnerContract = await getTitleEscrowContract(registryContract, tokenId); + }); + describe("Reject Transfer Beneficiary", () => { + let newBeneficiary: SignerWithAddress; + let prevBeneficiary: SignerWithAddress; + + beforeEach(async () => { + [newBeneficiary] = users.others; + + await titleEscrowOwnerContract + .connect(users.beneficiary) + .nominate(newBeneficiary.address, txnHexRemarks.nominateRemark); + await titleEscrowOwnerContract + .connect(users.holder) + .transferBeneficiary(newBeneficiary.address, txnHexRemarks.beneficiaryTransferRemark); + prevBeneficiary = users.beneficiary; + }); + it("should have valid previous and new beneficiary", async () => { + const previousBeneficiary = await titleEscrowOwnerContract.prevBeneficiary(); + expect(previousBeneficiary).to.equal(users.beneficiary.address); + expect(await titleEscrowOwnerContract.beneficiary()).to.equal(newBeneficiary.address); + }); + + it("should not allow non-beneficiary to reject beneficiary transfer", async () => { + const tx = titleEscrowOwnerContract + .connect(users.holder) + .rejectTransferBeneficiary(txnHexRemarks.rejectTransferRemark); + + await expect(tx).to.be.revertedWithCustomError(titleEscrowOwnerContract, "CallerNotBeneficiary"); + }); + + it("should not allow rejecting beneficiary transfer when there is no pending transfer", async () => { + await titleEscrowOwnerContract + .connect(newBeneficiary) + .rejectTransferBeneficiary(txnHexRemarks.rejectTransferRemark); + const tx = titleEscrowOwnerContract + .connect(users.beneficiary) + .rejectTransferBeneficiary(txnHexRemarks.rejectTransferRemark); + + await expect(tx).to.be.revertedWithCustomError(titleEscrowOwnerContract, "InvalidTransferToZeroAddress"); + }); + + it("should allow beneficiary to reject transfer beneficiary ", async () => { + const tx = titleEscrowOwnerContract + .connect(newBeneficiary) + .rejectTransferBeneficiary(txnHexRemarks.rejectTransferRemark); + + await expect(tx) + .to.emit(titleEscrowOwnerContract, "RejectTransferBeneficiary") + .withArgs( + newBeneficiary.address, + prevBeneficiary.address, + registryContract.address, + tokenId, + txnHexRemarks.rejectTransferRemark + ); + expect(await titleEscrowOwnerContract.beneficiary()).to.equal(prevBeneficiary.address); + expect(await titleEscrowOwnerContract.prevBeneficiary()).to.equal(defaultAddress.Zero); + expect(await titleEscrowOwnerContract.remark()).to.equal(txnHexRemarks.rejectTransferRemark); + }); + }); + describe("Reject Transfer Holder", () => { + let newHolder: SignerWithAddress; + let prevHolder: SignerWithAddress; + beforeEach(async () => { + [newHolder] = users.others; + await titleEscrowOwnerContract + .connect(users.holder) + .transferHolder(newHolder.address, txnHexRemarks.holderTransferRemark); + titleEscrowOwnerContract = await getTitleEscrowContract(registryContract, tokenId); + prevHolder = users.holder; + }); + it("should have a valid previous holder", async () => { + expect(await titleEscrowOwnerContract.prevHolder()).to.equal(prevHolder.address); + expect(await titleEscrowOwnerContract.holder()).to.equal(newHolder.address); + }); + it("should not allow non-holder to reject holder transfer", async () => { + const tx = titleEscrowOwnerContract + .connect(users.beneficiary) + .rejectTransferHolder(txnHexRemarks.rejectTransferRemark); + + await expect(tx).to.be.revertedWithCustomError(titleEscrowOwnerContract, "CallerNotHolder"); + }); + it("should not allow holder to reject holder transfer when there is no pending transfer", async () => { + await titleEscrowOwnerContract.connect(newHolder).rejectTransferHolder(txnHexRemarks.rejectTransferRemark); + const tx = titleEscrowOwnerContract + .connect(users.holder) + .rejectTransferHolder(txnHexRemarks.rejectTransferRemark); + + await expect(tx).to.be.revertedWithCustomError(titleEscrowOwnerContract, "InvalidTransferToZeroAddress"); + }); + it("should allow holder to reject holder transfer", async () => { + await titleEscrowOwnerContract.connect(newHolder).rejectTransferHolder(txnHexRemarks.rejectTransferRemark); + expect(await titleEscrowOwnerContract.holder()).to.equal(users.holder.address); + expect(await titleEscrowOwnerContract.prevHolder()).to.equal(defaultAddress.Zero); + expect(await titleEscrowOwnerContract.remark()).to.equal(txnHexRemarks.rejectTransferRemark); + }); + }); + describe("1: Reject Transfer Owners when previous holder and owner are different", () => { + let newBeneficiary: SignerWithAddress; + let newHolder: SignerWithAddress; + let previousHolder: SignerWithAddress; + let previousBeneficiary: SignerWithAddress; + let nonOwner: SignerWithAddress; + beforeEach(async () => { + [newBeneficiary, newHolder, nonOwner] = users.others; + newHolder = newBeneficiary; + await titleEscrowOwnerContract + .connect(users.beneficiary) + .nominate(newBeneficiary.address, txnHexRemarks.nominateRemark); + await titleEscrowOwnerContract + .connect(users.holder) + .transferBeneficiary(newBeneficiary.address, txnHexRemarks.beneficiaryTransferRemark); + await titleEscrowOwnerContract + .connect(users.holder) + .transferHolder(newHolder.address, txnHexRemarks.holderTransferRemark); + previousHolder = users.holder; + previousBeneficiary = users.beneficiary; + }); + it("should have correct prev and new holder/beneficiary", async () => { + const [currentBeneficiary, currentHolder, _prevBeneficiary, _prevHolder] = await Promise.all([ + titleEscrowOwnerContract.beneficiary(), + titleEscrowOwnerContract.holder(), + titleEscrowOwnerContract.prevBeneficiary(), + titleEscrowOwnerContract.prevHolder(), + ]); + expect(currentBeneficiary).to.equal(newBeneficiary.address); + expect(currentHolder).to.equal(newHolder.address); + expect(_prevBeneficiary).to.equal(users.beneficiary.address); + expect(_prevHolder).to.equal(users.holder.address); + }); + it("should not allow non-owner to reject transfer owners", async () => { + const tx = titleEscrowOwnerContract + .connect(nonOwner) + .rejectTransferOwners(txnHexRemarks.rejectTransferRemark); + + await expect(tx).to.be.revertedWithCustomError(titleEscrowOwnerContract, "CallerNotBeneficiary"); + }); + it("should not allow holder to reject only holder transfer", async () => { + const tx = titleEscrowOwnerContract + .connect(newHolder) + .rejectTransferHolder(txnHexRemarks.rejectTransferRemark); + + await expect(tx).to.be.revertedWithCustomError(titleEscrowOwnerContract, "DualRoleRejectionRequired"); + }); + it("should not allow beneficiary to reject only beneficiary transfer", async () => { + const tx = titleEscrowOwnerContract + .connect(newBeneficiary) + .rejectTransferBeneficiary(txnHexRemarks.rejectTransferRemark); + + await expect(tx).to.be.revertedWithCustomError(titleEscrowOwnerContract, "DualRoleRejectionRequired"); + }); + it("should allow owner to reject transfer owners", async () => { + expect( + await titleEscrowOwnerContract.connect(newHolder).rejectTransferOwners(txnHexRemarks.rejectTransferRemark) + ) + .to.emit(titleEscrowOwnerContract, "RejectTransferOwners") + .withArgs( + newBeneficiary.address, + previousBeneficiary.address, + newHolder.address, + previousHolder.address, + registryContract.address, + tokenId, + txnHexRemarks.rejectTransferRemark + ); + expect(await titleEscrowOwnerContract.holder()).to.equal(previousHolder.address); + expect(await titleEscrowOwnerContract.beneficiary()).to.equal(previousBeneficiary.address); + expect(await titleEscrowOwnerContract.prevHolder()).to.equal(defaultAddress.Zero); + expect(await titleEscrowOwnerContract.prevBeneficiary()).to.equal(defaultAddress.Zero); + }); + }); + describe("2: Reject Transfer Owners when previous holder and owner are same", () => { + let prevOwner: SignerWithAddress; + let newOwner: SignerWithAddress; + let nonOwner: SignerWithAddress; + beforeEach(async () => { + [newOwner, nonOwner] = users.others; + + // setting up the title escrow with same holder and beneficiary + await titleEscrowOwnerContract + .connect(users.beneficiary) + .nominate(users.holder.address, txnHexRemarks.nominateRemark); + await titleEscrowOwnerContract + .connect(users.holder) + .transferBeneficiary(users.holder.address, txnHexRemarks.beneficiaryTransferRemark); + + // setting up the new holder and beneficiary + await titleEscrowOwnerContract + .connect(users.holder) + .transferOwners(newOwner.address, newOwner.address, txnHexRemarks.transferOwnersRemark); + + prevOwner = users.holder; + }); + it("should have correct prev and new holder/beneficiary", async () => { + const previousHolder = await titleEscrowOwnerContract.prevHolder(); + const previousBeneficiary = await titleEscrowOwnerContract.prevBeneficiary(); + const holder = await titleEscrowOwnerContract.holder(); + const beneficiary = await titleEscrowOwnerContract.beneficiary(); + expect(previousHolder).to.equal(prevOwner.address); + expect(previousBeneficiary).to.equal(prevOwner.address); + expect(holder).to.equal(newOwner.address); + expect(beneficiary).to.equal(newOwner.address); + }); + it("should not allow non-owner to reject transfer owners", async () => { + const tx = titleEscrowOwnerContract + .connect(nonOwner) + .rejectTransferOwners(txnHexRemarks.rejectTransferRemark); + + await expect(tx).to.be.revertedWithCustomError(titleEscrowOwnerContract, "CallerNotBeneficiary"); + }); + it("should not allow new Owner to reject only holder transfer", async () => { + const tx = titleEscrowOwnerContract + .connect(newOwner) + .rejectTransferHolder(txnHexRemarks.rejectTransferRemark); + + await expect(tx).to.be.revertedWithCustomError(titleEscrowOwnerContract, "DualRoleRejectionRequired"); + }); + it("should not allow new Owner to reject only beneficiary transfer", async () => { + const tx = titleEscrowOwnerContract + .connect(newOwner) + .rejectTransferBeneficiary(txnHexRemarks.rejectTransferRemark); + + await expect(tx).to.be.revertedWithCustomError(titleEscrowOwnerContract, "DualRoleRejectionRequired"); + }); + it("should allow new owner to reject transfer owners", async () => { + expect( + await titleEscrowOwnerContract.connect(newOwner).rejectTransferOwners(txnHexRemarks.rejectTransferRemark) + ) + .to.emit(titleEscrowOwnerContract, "RejectTransferOwners") + .withArgs( + newOwner.address, + prevOwner.address, + newOwner.address, + prevOwner.address, + registryContract.address, + tokenId, + txnHexRemarks.rejectTransferRemark + ); + expect(await titleEscrowOwnerContract.holder()).to.equal(prevOwner.address); + expect(await titleEscrowOwnerContract.beneficiary()).to.equal(prevOwner.address); + expect(await titleEscrowOwnerContract.prevHolder()).to.equal(defaultAddress.Zero); + expect(await titleEscrowOwnerContract.prevBeneficiary()).to.equal(defaultAddress.Zero); + expect(await titleEscrowOwnerContract.remark()).to.equal(txnHexRemarks.rejectTransferRemark); + }); + }); + }); + + describe("Revert Rejection after any transaction", () => { + beforeEach(async () => { + await registryContract + .connect(users.carrier) + .mint(users.beneficiary.address, users.beneficiary.address, tokenId, txnHexRemarks.mintRemark); + titleEscrowOwnerContract = await getTitleEscrowContract(registryContract, tokenId); + }); + describe("when holder and beneficiary are different", () => { + let beneficiary: SignerWithAddress; + let holder: SignerWithAddress; + let prevBeneficiary: SignerWithAddress; + let prevHolder: SignerWithAddress; + let newBeneficiary: SignerWithAddress; + let newHolder: SignerWithAddress; + beforeEach(async () => { + [holder, beneficiary, newBeneficiary, newHolder] = users.others; + await titleEscrowOwnerContract + .connect(users.beneficiary) + .transferOwners(beneficiary.address, holder.address, txnHexRemarks.transferOwnersRemark); + prevBeneficiary = users.beneficiary; + prevHolder = users.beneficiary; + }); + it("should have correct initial values", async () => { + const [currentBeneficiary, currentHolder, _prevBeneficiary, _prevHolder] = await Promise.all([ + titleEscrowOwnerContract.beneficiary(), + titleEscrowOwnerContract.holder(), + titleEscrowOwnerContract.prevBeneficiary(), + titleEscrowOwnerContract.prevHolder(), + ]); + expect(currentBeneficiary).to.equal(beneficiary.address); + expect(currentHolder).to.equal(holder.address); + expect(_prevBeneficiary).to.equal(prevBeneficiary.address); + expect(_prevHolder).to.equal(prevHolder.address); + }); + it("should reset prevBeneficiary upon nomination", async () => { + await titleEscrowOwnerContract + .connect(beneficiary) + .nominate(newBeneficiary.address, txnHexRemarks.nominateRemark); + const [currentBeneficiary, currentHolder, _prevBeneficiary, _prevHolder] = await Promise.all([ + titleEscrowOwnerContract.beneficiary(), + titleEscrowOwnerContract.holder(), + titleEscrowOwnerContract.prevBeneficiary(), + titleEscrowOwnerContract.prevHolder(), + ]); + expect(currentBeneficiary).to.equal(beneficiary.address); + expect(currentHolder).to.equal(holder.address); + expect(_prevBeneficiary).to.equal(defaultAddress.Zero); + expect(_prevHolder).to.equal(prevHolder.address); + }); + it("should set prevBeneficiary and reset prevHolder upon endorsing nominee", async () => { + await titleEscrowOwnerContract + .connect(beneficiary) + .nominate(newBeneficiary.address, txnHexRemarks.nominateRemark); + expect(await titleEscrowOwnerContract.prevBeneficiary()).to.equal(defaultAddress.Zero); + await titleEscrowOwnerContract + .connect(holder) + .transferBeneficiary(newBeneficiary.address, txnHexRemarks.beneficiaryTransferRemark); + const [currentBeneficiary, currentHolder, _prevBeneficiary, _prevHolder] = await Promise.all([ + titleEscrowOwnerContract.beneficiary(), + titleEscrowOwnerContract.holder(), + titleEscrowOwnerContract.prevBeneficiary(), + titleEscrowOwnerContract.prevHolder(), + ]); + expect(currentBeneficiary).to.equal(newBeneficiary.address); + expect(currentHolder).to.equal(holder.address); + expect(_prevBeneficiary).to.equal(beneficiary.address); + expect(_prevHolder).to.equal(defaultAddress.Zero); + }); + it("should set prevHolder upon holder transfer", async () => { + await titleEscrowOwnerContract + .connect(holder) + .transferHolder(newHolder.address, txnHexRemarks.holderTransferRemark); + const [currentBeneficiary, currentHolder, _prevBeneficiary, _prevHolder] = await Promise.all([ + titleEscrowOwnerContract.beneficiary(), + titleEscrowOwnerContract.holder(), + titleEscrowOwnerContract.prevBeneficiary(), + titleEscrowOwnerContract.prevHolder(), + ]); + expect(currentBeneficiary).to.equal(beneficiary.address); + expect(currentHolder).to.equal(newHolder.address); + expect(_prevBeneficiary).to.equal(prevBeneficiary.address); + expect(_prevHolder).to.equal(holder.address); + }); + it("should have correct values upon owners transfer", async () => { + await titleEscrowOwnerContract + .connect(beneficiary) + .nominate(newBeneficiary.address, txnHexRemarks.nominateRemark); + await titleEscrowOwnerContract + .connect(holder) + .transferOwners(newBeneficiary.address, newHolder.address, txnHexRemarks.transferOwnersRemark); + const [currentBeneficiary, currentHolder, _prevBeneficiary, _prevHolder] = await Promise.all([ + titleEscrowOwnerContract.beneficiary(), + titleEscrowOwnerContract.holder(), + titleEscrowOwnerContract.prevBeneficiary(), + titleEscrowOwnerContract.prevHolder(), + ]); + expect(currentBeneficiary).to.equal(newBeneficiary.address); + expect(currentHolder).to.equal(newHolder.address); + expect(_prevBeneficiary).to.equal(beneficiary.address); + expect(_prevHolder).to.equal(holder.address); + }); + }); + describe("when holder and beneficiary are same", () => { + let owner: SignerWithAddress; // is both beneficiary and holder + let newHolder: SignerWithAddress; + let newBeneficiary: SignerWithAddress; + let prevBeneficiary: SignerWithAddress; + let prevHolder: SignerWithAddress; + beforeEach(async () => { + [owner, newBeneficiary, newHolder] = users.others; + await titleEscrowOwnerContract + .connect(users.beneficiary) + .transferOwners(owner.address, owner.address, txnHexRemarks.transferOwnersRemark); + prevBeneficiary = users.beneficiary; + prevHolder = users.beneficiary; + }); + it("should have correct initial values", async () => { + const [currentBeneficiary, currentHolder, _prevBeneficiary, _prevHolder] = await Promise.all([ + titleEscrowOwnerContract.beneficiary(), + titleEscrowOwnerContract.holder(), + titleEscrowOwnerContract.prevBeneficiary(), + titleEscrowOwnerContract.prevHolder(), + ]); + expect(currentBeneficiary).to.equal(owner.address); + expect(currentHolder).to.equal(owner.address); + expect(_prevBeneficiary).to.equal(prevBeneficiary.address); + expect(_prevHolder).to.equal(prevHolder.address); + }); + it("should reset both prevBeneficiary and prevHolder upon nomination", async () => { + await titleEscrowOwnerContract.connect(owner).nominate(newBeneficiary.address, txnHexRemarks.nominateRemark); + const [currentBeneficiary, currentHolder, _prevBeneficiary, _prevHolder] = await Promise.all([ + titleEscrowOwnerContract.beneficiary(), + titleEscrowOwnerContract.holder(), + titleEscrowOwnerContract.prevBeneficiary(), + titleEscrowOwnerContract.prevHolder(), + ]); + expect(currentBeneficiary).to.equal(owner.address); + expect(currentHolder).to.equal(owner.address); + expect(_prevBeneficiary).to.equal(defaultAddress.Zero); + expect(_prevHolder).to.equal(defaultAddress.Zero); + }); + it("should reset prevBeneficiary upon holder transfer", async () => { + await titleEscrowOwnerContract + .connect(owner) + .transferHolder(newHolder.address, txnHexRemarks.holderTransferRemark); + const [currentBeneficiary, currentHolder, _prevBeneficiary, _prevHolder] = await Promise.all([ + titleEscrowOwnerContract.beneficiary(), + titleEscrowOwnerContract.holder(), + titleEscrowOwnerContract.prevBeneficiary(), + titleEscrowOwnerContract.prevHolder(), + ]); + expect(currentBeneficiary).to.equal(owner.address); + expect(currentHolder).to.equal(newHolder.address); + expect(_prevBeneficiary).to.equal(defaultAddress.Zero); + expect(_prevHolder).to.equal(owner.address); + }); + it("should reset prevHolder upon beneficiary transfer", async () => { + await titleEscrowOwnerContract + .connect(owner) + .transferBeneficiary(newBeneficiary.address, txnHexRemarks.beneficiaryTransferRemark); + const [currentBeneficiary, currentHolder, _prevBeneficiary, _prevHolder] = await Promise.all([ + titleEscrowOwnerContract.beneficiary(), + titleEscrowOwnerContract.holder(), + titleEscrowOwnerContract.prevBeneficiary(), + titleEscrowOwnerContract.prevHolder(), + ]); + expect(currentBeneficiary).to.equal(newBeneficiary.address); + expect(currentHolder).to.equal(owner.address); + expect(_prevBeneficiary).to.equal(owner.address); + expect(_prevHolder).to.equal(defaultAddress.Zero); + }); + it("should reset both prevBeneficiary and prevHolder upon surrender", async () => { + await titleEscrowOwnerContract.connect(owner).surrender(txnHexRemarks.surrenderRemark); + const [currentBeneficiary, currentHolder, _prevBeneficiary, _prevHolder] = await Promise.all([ + titleEscrowOwnerContract.beneficiary(), + titleEscrowOwnerContract.holder(), + titleEscrowOwnerContract.prevBeneficiary(), + titleEscrowOwnerContract.prevHolder(), + ]); + expect(currentBeneficiary).to.equal(owner.address); + expect(currentHolder).to.equal(owner.address); + expect(_prevBeneficiary).to.equal(defaultAddress.Zero); + expect(_prevHolder).to.equal(defaultAddress.Zero); + }); + }); + }); + describe("Surrendering", () => { let beneficiary: SignerWithAddress; let holder: SignerWithAddress; beforeEach(async () => { // eslint-disable-next-line no-multi-assign - beneficiary = holder = users.others[faker.datatype.number(users.others.length - 1)]; + beneficiary = holder = users.others[users.others.length - 1]; await registryContract .connect(users.carrier) .mint(beneficiary.address, holder.address, tokenId, txnHexRemarks.mintRemark); @@ -954,6 +1438,21 @@ describe("Title Escrow", async () => { .to.emit(titleEscrowOwnerContract, "Surrender") .withArgs(beneficiary.address, registryContract.address, tokenId, txnHexRemarks.surrenderRemark); }); + it("should reset previous beneficiary and holder", async () => { + const [newOwner] = users.others; + // holder == beneficiary + await titleEscrowOwnerContract + .connect(beneficiary) + .transferOwners(newOwner.address, newOwner.address, txnHexRemarks.transferOwnersRemark); + + expect(await titleEscrowOwnerContract.prevBeneficiary()).to.equal(beneficiary.address); + expect(await titleEscrowOwnerContract.prevHolder()).to.equal(holder.address); + + await titleEscrowOwnerContract.connect(newOwner).surrender(txnHexRemarks.surrenderRemark); + + expect(await titleEscrowOwnerContract.prevBeneficiary()).to.equal(defaultAddress.Zero); + expect(await titleEscrowOwnerContract.prevHolder()).to.equal(defaultAddress.Zero); + }); }); describe("Shredding", () => { diff --git a/test/helpers/helpers.ts b/test/helpers/helpers.ts index 841ac5cf..441126b5 100644 --- a/test/helpers/helpers.ts +++ b/test/helpers/helpers.ts @@ -39,6 +39,7 @@ export const remarkString = { restorerRemark: "Remark: Document restored to reactivate it for further processing or corrections.", pauseRemark: "Remark: Registry paused till further processing or corrections.", unPauseRemark: "Remark: Registry unpaused to resume transactions.", + rejectTransferRemark: "Remark: Transfer of the document rejected due to discrepancies or errors in the request.", }; // Define the type for remarkString keys