Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 32 additions & 1 deletion src/libs/conditions/NonUSNationalityCondition.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -21,6 +25,8 @@ contract NonUSNationalityCondition is BaseCondition, BorgAuthACL {
error ProofExpired();
error ProofAlreadyUsed();
error MaxValidityPeriodExceeded();
error InvalidManager();
error InvalidInvestor();

event ProofSubmitted(
address indexed account,
Expand All @@ -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 =
Expand All @@ -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;

Expand Down Expand Up @@ -149,6 +163,22 @@ 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);
}

/// @notice Condition check used by LexScroWLite.conditionCheck
function checkCondition(
address _contract,
Expand All @@ -158,7 +188,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;
}

}
38 changes: 38 additions & 0 deletions test/NonUSNationalityConditionForkTest.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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.founderOverrides(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)
Expand Down
134 changes: 134 additions & 0 deletions test/NonUSNationalityConditionTest.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -307,6 +333,114 @@ 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.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));
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.founderOverrides(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 {
Expand Down
Loading