From 1f36f09464096e2c7baf950e31aec9dd1bafae01 Mon Sep 17 00:00:00 2001 From: detoo Date: Tue, 28 Apr 2026 17:06:40 -0700 Subject: [PATCH 1/2] feat: add founder overrides --- .../conditions/NonUSNationalityCondition.sol | 37 +++++- test/NonUSNationalityConditionForkTest.t.sol | 38 ++++++ test/NonUSNationalityConditionTest.t.sol | 114 ++++++++++++++++++ 3 files changed, 188 insertions(+), 1 deletion(-) diff --git a/src/libs/conditions/NonUSNationalityCondition.sol b/src/libs/conditions/NonUSNationalityCondition.sol index 330faa28..3b28aae9 100644 --- a/src/libs/conditions/NonUSNationalityCondition.sol +++ b/src/libs/conditions/NonUSNationalityCondition.sol @@ -8,6 +8,10 @@ import "../LexScroWLite.sol"; import "../auth.sol"; import "../../interfaces/IZKPassportVerifier.sol"; +interface ICyberCorpManager { + function AUTH() external view returns (address); +} + /// @title NonUSNationalityCondition /// @notice Round condition requiring a valid, non-US ZKPassport proof for the participant contract NonUSNationalityCondition is BaseCondition, BorgAuthACL { @@ -21,6 +25,8 @@ contract NonUSNationalityCondition is BaseCondition, BorgAuthACL { error ProofExpired(); error ProofAlreadyUsed(); error MaxValidityPeriodExceeded(); + error InvalidManager(); + error InvalidInvestor(); event ProofSubmitted( address indexed account, @@ -29,6 +35,12 @@ contract NonUSNationalityCondition is BaseCondition, BorgAuthACL { event MaxValidityPeriodUpdated(uint256 maxValidityPeriod); event ExcludedCountriesUpdated(string[] countries); + event FounderOverrideUpdated( + address indexed manager, + address indexed investor, + bool approved, + address indexed approver + ); // Deterministic verifier address from ZKPassport docs. address public constant DEFAULT_ZKPASSPORT_VERIFIER = @@ -41,6 +53,8 @@ contract NonUSNationalityCondition is BaseCondition, BorgAuthACL { mapping(address => uint256) public proofExpiry; mapping(bytes32 => bool) public usedProofIdentifiers; + // manager → investor → approved + mapping(address => mapping(address => bool)) public founderOverrides; string[] public excludedCountries; @@ -149,6 +163,26 @@ contract NonUSNationalityCondition is BaseCondition, BorgAuthACL { emit ProofSubmitted(msg.sender, expiresAt); } + function setFounderOverride( + address _manager, + address _investor, + bool _approved + ) external { + if (_manager == address(0)) revert InvalidManager(); + if (_investor == address(0)) revert InvalidInvestor(); + + // only the manager of the deal/round can set overrides + BorgAuth auth = BorgAuth(ICyberCorpManager(_manager).AUTH()); + auth.onlyRole(auth.OWNER_ROLE(), msg.sender); + + founderOverrides[_manager][_investor] = _approved; + emit FounderOverrideUpdated(_manager, _investor, _approved, msg.sender); + } + + function isFounderOverrideApproved(address _manager, address _investor) external view returns (bool) { + return founderOverrides[_manager][_investor]; + } + /// @notice Condition check used by LexScroWLite.conditionCheck function checkCondition( address _contract, @@ -158,7 +192,8 @@ contract NonUSNationalityCondition is BaseCondition, BorgAuthACL { LexScroWLite lexScrow = LexScroWLite(_contract); bytes32 agreementId = abi.decode(data, (bytes32)); address counterparty = lexScrow.getEscrowDetails(agreementId).counterParty; + // check overrides first, then the ZK proof + if (founderOverrides[_contract][counterparty]) return true; return proofExpiry[counterparty] >= block.timestamp; } - } diff --git a/test/NonUSNationalityConditionForkTest.t.sol b/test/NonUSNationalityConditionForkTest.t.sol index 5c364ece..64fb5713 100644 --- a/test/NonUSNationalityConditionForkTest.t.sol +++ b/test/NonUSNationalityConditionForkTest.t.sol @@ -16,6 +16,31 @@ import {NonUSNationalityCondition} from "../src/libs/conditions/NonUSNationality import {BorgAuth} from "../src/libs/auth.sol"; import {stdJson} from "forge-std/StdJson.sol"; +contract MockManager { + BorgAuth public AUTH; + mapping(bytes32 => address) public counterpartyByAgreementId; + + constructor(address auth) { AUTH = BorgAuth(auth); } + + function setCounterparty(bytes32 agreementId, address counterparty) external { + counterpartyByAgreementId[agreementId] = counterparty; + } + + function getEscrowDetails(bytes32 agreementId) external view returns (Escrow memory esc) { + Token[] memory corpAssets = new Token[](0); + Token[] memory buyerAssets = new Token[](0); + esc = Escrow({ + agreementId: agreementId, + counterParty: counterpartyByAgreementId[agreementId], + corpAssets: corpAssets, + buyerAssets: buyerAssets, + signature: "", + expiry: block.timestamp + 1 days, + status: EscrowStatus.PAID + }); + } +} + library NonUSNationalityConditionHelper { using stdJson for string; @@ -107,6 +132,19 @@ contract NonUSNationalityConditionForkTest is Test { assertEq(condition.proofExpiry(account), signedTimestamp + params.serviceConfig.validityPeriodInSeconds, "unexpected proof expiry"); } + function test_FounderOverride_HappyPath() public { + BorgAuth managerAuth = new BorgAuth(address(this)); + MockManager manager = new MockManager(address(managerAuth)); + address investor = address(0xA11CE); + bytes32 agreementId = keccak256("agreement-fork-override"); + manager.setCounterparty(agreementId, investor); + + condition.setFounderOverride(address(manager), investor, true); + + assertTrue(condition.isFounderOverrideApproved(address(manager), investor)); + assertTrue(condition.checkCondition(address(manager), bytes4(0), abi.encode(agreementId))); + } + /// @notice Real proof of non-FRA nationality should not pass since we want non-US + non-sanctioned proof function test_RevertIf_RealProofInvalid() public { // Assume the sample data is signed for Sepolia (included in committedInputs) diff --git a/test/NonUSNationalityConditionTest.t.sol b/test/NonUSNationalityConditionTest.t.sol index c3202fdc..a86d6bc0 100644 --- a/test/NonUSNationalityConditionTest.t.sol +++ b/test/NonUSNationalityConditionTest.t.sol @@ -110,6 +110,32 @@ contract MockZKPassportVerifier is IZKPassportVerifier { } } +// TODO rename it +contract MockManager { + BorgAuth public AUTH; + mapping(bytes32 => address) public counterpartyByAgreementId; + + constructor(address auth) { AUTH = BorgAuth(auth); } + + function setCounterparty(bytes32 agreementId, address counterparty) external { + counterpartyByAgreementId[agreementId] = counterparty; + } + + function getEscrowDetails(bytes32 agreementId) external view returns (Escrow memory esc) { + Token[] memory corpAssets = new Token[](0); + Token[] memory buyerAssets = new Token[](0); + esc = Escrow({ + agreementId: agreementId, + counterParty: counterpartyByAgreementId[agreementId], + corpAssets: corpAssets, + buyerAssets: buyerAssets, + signature: "", + expiry: block.timestamp + 1 days, + status: EscrowStatus.PAID + }); + } +} + contract NonUSNationalityConditionTest is Test { string internal constant EXPECTED_DOMAIN = "app.example"; string internal constant EXPECTED_SCOPE = "non-us-round"; @@ -307,6 +333,94 @@ contract NonUSNationalityConditionTest is Test { assertFalse(result); } + // --- founder override tests --- + + function test_SetFounderOverride_HappyPath() public { + BorgAuth managerAuth = new BorgAuth(address(this)); + MockManager manager = new MockManager(address(managerAuth)); + address investor = address(0xA11CE); + bytes32 agreementId = keccak256("agreement-override"); + manager.setCounterparty(agreementId, investor); + + condition.setFounderOverride(address(manager), investor, true); + + assertTrue(condition.isFounderOverrideApproved(address(manager), investor)); + assertTrue(condition.checkCondition(address(manager), bytes4(0), abi.encode(agreementId))); + } + + function test_SetFounderOverride_RevokeOverride() public { + BorgAuth managerAuth = new BorgAuth(address(this)); + MockManager manager = new MockManager(address(managerAuth)); + address investor = address(0xA11CE); + bytes32 agreementId = keccak256("agreement-revoke"); + manager.setCounterparty(agreementId, investor); + + condition.setFounderOverride(address(manager), investor, true); + condition.setFounderOverride(address(manager), investor, false); + + assertFalse(condition.isFounderOverrideApproved(address(manager), investor)); + assertFalse(condition.checkCondition(address(manager), bytes4(0), abi.encode(agreementId))); + } + + function test_CheckCondition_FounderOverrideTakesPrecedence() public { + BorgAuth managerAuth = new BorgAuth(address(this)); + MockManager manager = new MockManager(address(managerAuth)); + address investor = address(0xA11CE); + bytes32 agreementId = keccak256("agreement-expired"); + manager.setCounterparty(agreementId, investor); + + ProofVerificationParams memory params = _buildParams(investor, block.timestamp, 1 days); + vm.prank(investor); + condition.submitProof(params, false); + + vm.warp(block.timestamp + 2 days); + assertFalse(condition.checkCondition(address(manager), bytes4(0), abi.encode(agreementId))); + + condition.setFounderOverride(address(manager), investor, true); + assertTrue(condition.checkCondition(address(manager), bytes4(0), abi.encode(agreementId))); + } + + function test_CheckCondition_OverrideScopedToManager() public { + BorgAuth managerAuthA = new BorgAuth(address(this)); + MockManager managerA = new MockManager(address(managerAuthA)); + BorgAuth managerAuthB = new BorgAuth(address(this)); + MockManager managerB = new MockManager(address(managerAuthB)); + address investor = address(0xA11CE); + bytes32 agreementId = keccak256("agreement-scoped"); + managerA.setCounterparty(agreementId, investor); + managerB.setCounterparty(agreementId, investor); + + condition.setFounderOverride(address(managerA), investor, true); + + assertTrue(condition.checkCondition(address(managerA), bytes4(0), abi.encode(agreementId))); + assertFalse(condition.checkCondition(address(managerB), bytes4(0), abi.encode(agreementId))); + } + + function test_RevertWhen_SetFounderOverride_Unauthorized() public { + BorgAuth managerAuth = new BorgAuth(address(this)); + MockManager manager = new MockManager(address(managerAuth)); + address attacker = address(0xDEAD); + + vm.prank(attacker); + vm.expectRevert( + abi.encodeWithSelector(BorgAuth.BorgAuth_NotAuthorized.selector, uint256(99), attacker) + ); + condition.setFounderOverride(address(manager), address(0xA11CE), true); + } + + function test_RevertWhen_SetFounderOverride_InvalidManager() public { + vm.expectRevert(NonUSNationalityCondition.InvalidManager.selector); + condition.setFounderOverride(address(0), address(0xA11CE), true); + } + + function test_RevertWhen_SetFounderOverride_InvalidInvestor() public { + BorgAuth managerAuth = new BorgAuth(address(this)); + MockManager manager = new MockManager(address(managerAuth)); + + vm.expectRevert(NonUSNationalityCondition.InvalidInvestor.selector); + condition.setFounderOverride(address(manager), address(0), true); + } + // --- access control tests --- function test_RevertWhen_UpdateMaxValidityPeriod_Unauthorized() public { From 6d651dfc7055db2af5f86654e847c366a33ee372 Mon Sep 17 00:00:00 2001 From: detoo Date: Tue, 28 Apr 2026 21:08:59 -0700 Subject: [PATCH 2/2] feat: remove unnecessary functions. Add more tests --- .../conditions/NonUSNationalityCondition.sol | 4 ---- test/NonUSNationalityConditionForkTest.t.sol | 2 +- test/NonUSNationalityConditionTest.t.sol | 24 +++++++++++++++++-- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/src/libs/conditions/NonUSNationalityCondition.sol b/src/libs/conditions/NonUSNationalityCondition.sol index 3b28aae9..fc0a0870 100644 --- a/src/libs/conditions/NonUSNationalityCondition.sol +++ b/src/libs/conditions/NonUSNationalityCondition.sol @@ -179,10 +179,6 @@ contract NonUSNationalityCondition is BaseCondition, BorgAuthACL { emit FounderOverrideUpdated(_manager, _investor, _approved, msg.sender); } - function isFounderOverrideApproved(address _manager, address _investor) external view returns (bool) { - return founderOverrides[_manager][_investor]; - } - /// @notice Condition check used by LexScroWLite.conditionCheck function checkCondition( address _contract, diff --git a/test/NonUSNationalityConditionForkTest.t.sol b/test/NonUSNationalityConditionForkTest.t.sol index 64fb5713..ef765c40 100644 --- a/test/NonUSNationalityConditionForkTest.t.sol +++ b/test/NonUSNationalityConditionForkTest.t.sol @@ -141,7 +141,7 @@ contract NonUSNationalityConditionForkTest is Test { condition.setFounderOverride(address(manager), investor, true); - assertTrue(condition.isFounderOverrideApproved(address(manager), investor)); + assertTrue(condition.founderOverrides(address(manager), investor)); assertTrue(condition.checkCondition(address(manager), bytes4(0), abi.encode(agreementId))); } diff --git a/test/NonUSNationalityConditionTest.t.sol b/test/NonUSNationalityConditionTest.t.sol index a86d6bc0..ed712d3b 100644 --- a/test/NonUSNationalityConditionTest.t.sol +++ b/test/NonUSNationalityConditionTest.t.sol @@ -344,10 +344,30 @@ contract NonUSNationalityConditionTest is Test { condition.setFounderOverride(address(manager), investor, true); - assertTrue(condition.isFounderOverrideApproved(address(manager), investor)); + assertTrue(condition.founderOverrides(address(manager), investor)); assertTrue(condition.checkCondition(address(manager), bytes4(0), abi.encode(agreementId))); } + function test_SetFounderOverride_PublicMappingGetter() public { + BorgAuth managerAuth = new BorgAuth(address(this)); + MockManager manager = new MockManager(address(managerAuth)); + address investor = address(0xA11CE); + + assertFalse(condition.founderOverrides(address(manager), investor)); + condition.setFounderOverride(address(manager), investor, true); + assertTrue(condition.founderOverrides(address(manager), investor)); + } + + function test_SetFounderOverride_EmitsFounderOverrideUpdated() public { + BorgAuth managerAuth = new BorgAuth(address(this)); + MockManager manager = new MockManager(address(managerAuth)); + address investor = address(0xA11CE); + + vm.expectEmit(true, true, true, true); + emit NonUSNationalityCondition.FounderOverrideUpdated(address(manager), investor, true, address(this)); + condition.setFounderOverride(address(manager), investor, true); + } + function test_SetFounderOverride_RevokeOverride() public { BorgAuth managerAuth = new BorgAuth(address(this)); MockManager manager = new MockManager(address(managerAuth)); @@ -358,7 +378,7 @@ contract NonUSNationalityConditionTest is Test { condition.setFounderOverride(address(manager), investor, true); condition.setFounderOverride(address(manager), investor, false); - assertFalse(condition.isFounderOverrideApproved(address(manager), investor)); + assertFalse(condition.founderOverrides(address(manager), investor)); assertFalse(condition.checkCondition(address(manager), bytes4(0), abi.encode(agreementId))); }