",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "mocha"
8 | },
9 | "author": "",
10 | "license": "ISC",
11 | "dependencies": {
12 | "@metamask/eth-sig-util": "^7.0.3",
13 | "eth-sig-util": "^3.0.1",
14 | "ethers": "^6.13.3",
15 | "web3": "^4.13.0"
16 | },
17 | "keywords": [],
18 | "type": "module",
19 | "devDependencies": {
20 | "chai": "^5.1.1",
21 | "mocha": "^10.7.3",
22 | "wait-on": "^8.0.1"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/registry-contracts/agreementDetails.json:
--------------------------------------------------------------------------------
1 | {
2 | "agreementURI": "https://bafybeiakxvysdvsvupqcibkpifugzwcnllzt2udjk3l4yhcix7dqxxqyp4.ipfs.w3s.link/agreement.pdf",
3 | "bountyTerms": {
4 | "bountyCapUSD": ,
5 | "bountyPercentage": ,
6 | "diligenceRequirements": "",
7 | "identity": ,
8 | "retainable":
9 | },
10 | "chains": [
11 | {
12 | "accounts": [
13 | {
14 | "accountAddress": "",
15 | "childContractScope": ,
16 | "signature": "0x"
17 | },
18 | ],
19 | "assetRecoveryAddress": "",
20 | "id":
21 | }
22 | ],
23 | "contactDetails": [
24 | {
25 | "contact": "",
26 | "name": ""
27 | }
28 | ],
29 | "protocolName": ""
30 | }
--------------------------------------------------------------------------------
/js/fs.js:
--------------------------------------------------------------------------------
1 | import fs from 'fs';
2 | import {expect} from 'chai';
3 |
4 | export async function loadABI(path) {
5 | try {
6 | const artifact = JSON.parse(fs.readFileSync(path, 'utf8'));
7 | const contractABI = artifact.abi;
8 | return contractABI;
9 | } catch (error) {
10 | console.error('Error reading ABI:', error);
11 | return null;
12 | }
13 | }
14 |
15 | export async function loadAddress(path) {
16 | try {
17 | const run = JSON.parse(fs.readFileSync(path, 'utf8'));
18 | expect(run.transactions.length).to.equal(1);
19 | expect(run.transactions[0].contractName).to.equal("SafeHarborRegistry");
20 |
21 | const contractAddr = run.transactions[0].contractAddress;
22 | return contractAddr;
23 | } catch (error) {
24 | console.error('Error reading address:', error);
25 | return null;
26 | }
27 | }
--------------------------------------------------------------------------------
/registry-contracts/test/v1/mock.json:
--------------------------------------------------------------------------------
1 | {
2 | "protocolName": "testProtocol",
3 | "chains": [
4 | {
5 | "accounts": [
6 | {
7 | "accountAddress": "0x1111111111111111111111111111111111111111",
8 | "childContractScope": 2,
9 | "signature": ""
10 | }
11 | ],
12 | "assetRecoveryAddress": "0x0000000000000000000000000000000000000011",
13 | "id": 1
14 | }
15 | ],
16 | "contact": [
17 | {
18 | "name": "Test Name",
19 | "contact": "test@mail.com"
20 | }
21 | ],
22 | "bountyTerms": {
23 | "bountyPercentage": 10,
24 | "bountyCapUSD": 100,
25 | "retainable": true,
26 | "identity": 0,
27 | "diligenceRequirements": "none"
28 | },
29 | "agreementURI": "ipfs://testHash"
30 | }
--------------------------------------------------------------------------------
/registry-contracts/test/v2/mock.json:
--------------------------------------------------------------------------------
1 | {
2 | "protocolName": "testProtocolV2",
3 | "chains": [
4 | {
5 | "accounts": [
6 | {
7 | "accountAddress": "0x1111111111111111111111111111111111111111",
8 | "childContractScope": 2
9 | }
10 | ],
11 | "assetRecoveryAddress": "0x0000000000000000000000000000000000000022",
12 | "id": "eip155:1"
13 | }
14 | ],
15 | "contact": [
16 | {
17 | "name": "Test Name V2",
18 | "contact": "test@mail.com"
19 | }
20 | ],
21 | "bountyTerms": {
22 | "aggregateBountyCapUSD": 1000,
23 | "bountyPercentage": 10,
24 | "bountyCapUSD": 100,
25 | "retainable": false,
26 | "identity": 0,
27 | "diligenceRequirements": "none"
28 | },
29 | "agreementURI": "ipfs://testHash"
30 | }
--------------------------------------------------------------------------------
/registry-contracts/script/v2/GetAgreementDetailsV2.s.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: UNLICENSED
2 | pragma solidity ^0.8.13;
3 |
4 | import {console} from "forge-std/console.sol";
5 | import {ScriptBase} from "forge-std/Base.sol";
6 | import "../../src/v2/SafeHarborRegistryV2.sol";
7 | import "../../src/v2/AgreementV2.sol";
8 | import {logAgreementDetails} from "../../test/v2/mock.sol";
9 |
10 | contract GetAgreementDetailsV2 is ScriptBase {
11 | function run() public view {
12 | address agreementAddress = vm.envAddress("AGREEMENT_ADDRESS");
13 | run(agreementAddress);
14 | }
15 |
16 | /// @notice CLI entry: forge script ... --sig 'run(address)'
17 | function run(address agreementAddress) public view {
18 | AgreementV2 agreement = AgreementV2(agreementAddress);
19 | AgreementDetailsV2 memory details = agreement.getDetails();
20 | logAgreementDetails(details);
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/js/test.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Sets up anvil and deploys the SafeHarborRegistry smart contract
4 |
5 | # Function to clean up background processes on exit
6 | cleanup() {
7 | echo "Stopping all processes..."
8 | kill $anvil_pid $http_server_pid
9 | }
10 |
11 | # Set the trap to call cleanup on script exit
12 | trap cleanup EXIT
13 |
14 | # Change to the desired working directory for anvil and forge
15 | cd ../registry-contracts
16 |
17 | forge compile
18 |
19 | # Launch anvil in the background
20 | anvil --block-time 1 --port 8545 &
21 | anvil_pid=$!
22 |
23 | # Wait a bit to ensure anvil is up
24 | sleep 2
25 |
26 | # Deploy the smart contract
27 | export REGISTRY_DEPLOYER_PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
28 | forge script script/SafeHarborRegistryDeploy.s.sol:SafeHarborRegistryDeploy --rpc-url http://localhost:8545 --broadcast
29 |
30 | # execute the JS test
31 | cd ../js
32 |
33 | npm test
34 |
--------------------------------------------------------------------------------
/registry-contracts/script/v2/SetFallbackRegitry.s.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: UNLICENSED
2 | pragma solidity ^0.8.13;
3 |
4 | import {console} from "forge-std/console.sol";
5 | import {Script} from "forge-std/Script.sol";
6 | import {SafeHarborRegistryV2, IRegistry} from "../../src/v2/SafeHarborRegistryV2.sol";
7 |
8 | contract SetFallbackRegistry is Script {
9 | // Update these addresses to match your deployed contracts
10 | address constant REGISTRY_ADDRESS = 0x1eaCD100B0546E433fbf4d773109cAD482c34686;
11 | address constant FALLBACK_REGISTRY_ADDRESS = 0x8f72fcf695523A6FC7DD97EafDd7A083c386b7b6;
12 |
13 | function run() public {
14 | uint256 deployerPrivateKey = vm.envUint("REGISTRY_DEPLOYER_PRIVATE_KEY");
15 |
16 | SafeHarborRegistryV2 registry = SafeHarborRegistryV2(REGISTRY_ADDRESS);
17 |
18 | vm.broadcast(deployerPrivateKey);
19 | registry.setFallbackRegistry(IRegistry(FALLBACK_REGISTRY_ADDRESS));
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/registry-contracts/src/v2/AgreementFactoryV2.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity ^0.8.20;
3 |
4 | import {AgreementV2} from "./AgreementV2.sol";
5 | import {SafeHarborRegistryV2} from "./SafeHarborRegistryV2.sol";
6 | import {AgreementDetailsV2} from "./AgreementDetailsV2.sol";
7 |
8 | /// @title Factory for creating AgreementV2 contracts
9 | contract AgreementFactoryV2 {
10 | // ----- EXTERNAL FUNCTIONS -----
11 |
12 | /// @notice Creates an AgreementV2 contract.
13 | /// @param details The agreement details
14 | /// @param registry The Safe Harbor Registry V2 address
15 | /// @param owner The owner of the agreement
16 | function create(AgreementDetailsV2 memory details, address registry, address owner)
17 | external
18 | returns (address agreementAddress)
19 | {
20 | AgreementV2 agreement = new AgreementV2(details, registry, owner);
21 | agreementAddress = address(agreement);
22 | return agreementAddress;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/registry-contracts/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Security Alliance
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/registry-contracts/addChainsV2.json:
--------------------------------------------------------------------------------
1 | {
2 | "agreementAddress": "FILL ME IN",
3 | "chains": [
4 | {
5 | "accounts": [
6 | {
7 | "accountAddress": "0xe20fCBdBfFC4Dd138cE8b2E6FBb6CB49777ad64D",
8 | "childContractScope": 1
9 | },
10 | {
11 | "accountAddress": "0xA238Dd80C259a72e81d7e4664a9801593F98d1c5",
12 | "childContractScope": 2
13 | }
14 | ],
15 | "assetRecoveryAddress": "0xBA9424d650A4F5c80a0dA641254d1AcCE2A37057",
16 | "id": "eip155:8453"
17 | },
18 | {
19 | "accounts": [
20 | {
21 | "accountAddress": "0xff75B6da14FfbbfD355Daf7a2731456b3562Ba6D",
22 | "childContractScope": 1
23 | },
24 | {
25 | "accountAddress": "0x6807dc923806fE8Fd134338EABCA509979a7e0cB",
26 | "childContractScope": 2
27 | }
28 | ],
29 | "assetRecoveryAddress": "0x25Ec457d1778b0E5316e7f38f3c22baF413F1A8C",
30 | "id": "eip155:56"
31 | }
32 | ]
33 | }
--------------------------------------------------------------------------------
/js/types.js:
--------------------------------------------------------------------------------
1 | export const primaryType = "AgreementDetailsV1";
2 |
3 | export const types = {
4 | EIP712Domain: [
5 | { name: "name", type: "string" },
6 | { name: "version", type: "string" },
7 | { name: "chainId", type: "uint256" },
8 | { name: "verifyingContract", type: "address" },
9 | ],
10 | AgreementDetailsV1: [
11 | { name: "protocolName", type: "string" },
12 | { name: "contactDetails", type: "Contact[]" },
13 | { name: "chains", type: "Chain[]" },
14 | { name: "bountyTerms", type: "BountyTerms" },
15 | { name: "agreementURI", type: "string" }
16 | ],
17 | Contact: [
18 | { name: "name", type: "string" },
19 | { name: "contact", type: "string" }
20 | ],
21 | Chain: [
22 | { name: "assetRecoveryAddress", type: "address" },
23 | { name: "accounts", type: "Account[]" },
24 | { name: "id", type: "uint256" }
25 | ],
26 | Account: [
27 | { name: "accountAddress", type: "address" },
28 | { name: "childContractScope", type: "uint8" },
29 | { name: "signature", type: "bytes" }
30 | ],
31 | BountyTerms: [
32 | { name: "bountyPercentage", type: "uint256" },
33 | { name: "bountyCapUSD", type: "uint256" },
34 | { name: "retainable", type: "bool" },
35 | { name: "identity", type: "uint8" },
36 | { name: "diligenceRequirements", type: "string" }
37 | ]
38 | };
--------------------------------------------------------------------------------
/registry-contracts/src/v1/EIP712.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity ^0.8.20;
3 |
4 | import "./SignatureValidator.sol";
5 |
6 | contract EIP712 is SignatureValidator {
7 | struct EIP712Domain {
8 | string name;
9 | string version;
10 | uint256 chainId;
11 | address verifyingContract;
12 | }
13 |
14 | bytes32 private constant TYPEHASH =
15 | keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)");
16 |
17 | bytes32 private immutable _CACHED_DOMAIN_SEPARATOR;
18 | uint256 private immutable _CACHED_CHAIN_ID;
19 | string private _name;
20 | string private _version;
21 |
22 | constructor(string memory name, string memory version) {
23 | _name = name;
24 | _version = version;
25 | _CACHED_CHAIN_ID = block.chainid;
26 | _CACHED_DOMAIN_SEPARATOR = _buildDomainSeparator();
27 | }
28 |
29 | function DOMAIN_SEPARATOR() public view returns (bytes32) {
30 | if (block.chainid == _CACHED_CHAIN_ID) {
31 | return _CACHED_DOMAIN_SEPARATOR;
32 | } else {
33 | return _buildDomainSeparator();
34 | }
35 | }
36 |
37 | function _buildDomainSeparator() private view returns (bytes32) {
38 | bytes32 hashedName = keccak256(bytes(_name));
39 | bytes32 hashedVersion = keccak256(bytes(_version));
40 | return keccak256(abi.encode(TYPEHASH, hashedName, hashedVersion, block.chainid, address(this)));
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/.github/workflows/setchains.yaml:
--------------------------------------------------------------------------------
1 | name: Set Chains
2 |
3 | on:
4 | workflow_dispatch:
5 | inputs:
6 | rpc_url:
7 | description: "RPC URL"
8 | required: true
9 | type: string
10 | broadcast:
11 | description: "Include --broadcast flag"
12 | required: true
13 | default: false
14 | type: boolean
15 |
16 | jobs:
17 | set_chains:
18 | name: Set Chains V2
19 | runs-on: ubuntu-latest
20 | if: ${{ github.event.inputs.rpc_url != '' }}
21 | steps:
22 | - name: Checkout repository
23 | uses: actions/checkout@v4
24 | with:
25 | token: ${{ secrets.GITHUB_TOKEN }}
26 | fetch-depth: 0
27 | submodules: recursive
28 |
29 | - name: Install Foundry
30 | uses: foundry-rs/foundry-toolchain@v1
31 |
32 | - name: Set Chains V2 (Simulation)
33 | run: forge script SetChains --rpc-url ${{ github.event.inputs.rpc_url }} -vvvv
34 | working-directory: registry-contracts
35 | env:
36 | REGISTRY_DEPLOYER_PRIVATE_KEY: ${{ secrets.REGISTRY_DEPLOYER_PRIVATE_KEY }}
37 |
38 | - name: Set Chains V2 (Broadcast)
39 | if: ${{ github.event.inputs.broadcast == 'true' }}
40 | run: forge script SetChains --rpc-url ${{ github.event.inputs.rpc_url }} --broadcast
41 | working-directory: registry-contracts
42 | env:
43 | ETHERSCAN_API_KEY: ${{ secrets.ETHERSCAN_API_KEY }}
44 | REGISTRY_DEPLOYER_PRIVATE_KEY: ${{ secrets.REGISTRY_DEPLOYER_PRIVATE_KEY }}
45 |
--------------------------------------------------------------------------------
/.github/workflows/forge-fmt.yaml:
--------------------------------------------------------------------------------
1 | name: Auto Format Solidity
2 |
3 | on:
4 | push:
5 | branches: [main]
6 | paths:
7 | - "registry-contracts/**/*.sol"
8 | pull_request:
9 | branches: [main]
10 | paths:
11 | - "registry-contracts/**/*.sol"
12 |
13 | permissions:
14 | contents: write
15 | pull-requests: write
16 |
17 | jobs:
18 | format:
19 | name: Format Solidity files
20 | runs-on: ubuntu-latest
21 |
22 | steps:
23 | - name: Checkout repository
24 | uses: actions/checkout@v4
25 | with:
26 | token: ${{ secrets.GITHUB_TOKEN }}
27 | fetch-depth: 0
28 |
29 | - name: Install Foundry
30 | uses: foundry-rs/foundry-toolchain@v1
31 |
32 | - name: Run forge fmt
33 | run: forge fmt
34 | working-directory: registry-contracts
35 |
36 | - name: Check for changes
37 | id: verify-changed-files
38 | run: |
39 | if [ -n "$(git status --porcelain registry-contracts)" ]; then
40 | echo "changed=true" >> $GITHUB_OUTPUT
41 | else
42 | echo "changed=false" >> $GITHUB_OUTPUT
43 | fi
44 |
45 | - name: Commit formatted files
46 | if: steps.verify-changed-files.outputs.changed == 'true'
47 | uses: EndBug/add-and-commit@v9
48 | with:
49 | author_name: forge-fmt[bot]
50 | author_email: forge-fmt[bot]@users.noreply.github.com
51 | message: "style: auto-format Solidity files with forge fmt"
52 | add: "registry-contracts/**/*.sol"
53 | push: true
54 |
--------------------------------------------------------------------------------
/documents/exhibits/c.tex:
--------------------------------------------------------------------------------
1 | [\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_] [INSERT NAME OF INDIVIDUAL OR ENTITY] hereby acknowledges and agrees to, and consents to be bound by the terms and conditions of, that certain Safe Harbor Agreement for Whitehats, adopted by the Protocol Community on [\_\_\_\_\_\_\_\_\_\_] (the "\textbf{Whitehat Agreement}"), available here [\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_], as a "Security Team" and member of the "Protocol Community" thereunder. Without limiting the generality of the foregoing:
2 |
3 | \begin{itemize}
4 | \item the Security Team hereby consents to Whitehats attempting Eligible Funds Rescues of any and all Tokens deposited into the Protocol and the deduction of Bounties out of such Tokens to compensate Eligible Whitehats for successful Eligible Funds Rescues;
5 |
6 | \item the Security Team acknowledges and agrees that Tokens may be lost, stolen, suffer diminished value, or become disabled or frozen in connection with attempts at Eligible Funds Rescues, or that the functioning of the Protocol may be adversely affected; and
7 |
8 | \item the Security Team agrees to hold the other Protocol Community Members harmless from any loss, liability or other damages suffered by the Security Team in connection with attempted Eligible Funds Exploits under the Whitehat Agreement.
9 | \end{itemize}
10 |
11 | % COMMENT: The original document indicates "[ADD SIGNATURE BLOCKS]" but doesn't specify the format or content of these signature blocks. This would need to be customized based on the specific Security Team's requirements.
12 |
13 | [ADD SIGNATURE BLOCKS]
--------------------------------------------------------------------------------
/registry-contracts/script/v2/AddChainsV2.s.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: UNLICENSED
2 | pragma solidity ^0.8.13;
3 |
4 | import {Script} from "forge-std/Script.sol";
5 | import {console} from "forge-std/console.sol";
6 | import {stdJson} from "forge-std/StdJson.sol";
7 |
8 | import {AgreementV2} from "../../src/v2/AgreementV2.sol";
9 | import {AdoptSafeHarborV2} from "./AdoptSafeHarborV2.s.sol";
10 | import {Chain as ChainV2} from "../../src/v2/AgreementDetailsV2.sol";
11 |
12 | contract AddChainsV2 is Script {
13 | using stdJson for string;
14 |
15 | // Path to the JSON input file
16 | string constant INPUT_JSON_PATH = "addChainsV2.json";
17 |
18 | function run() public {
19 | uint256 pk = vm.envUint("DEPLOYER_PRIVATE_KEY");
20 | string memory json = vm.readFile(INPUT_JSON_PATH);
21 |
22 | // Read the agreement address from JSON
23 | address agreementAddress = json.readAddress(".agreementAddress");
24 | require(agreementAddress != address(0), "agreementAddress missing or zero");
25 |
26 | AgreementV2 agreement = AgreementV2(agreementAddress);
27 | require(address(agreement).code.length > 0, "No contract at agreementAddress");
28 |
29 | AdoptSafeHarborV2 parser = new AdoptSafeHarborV2();
30 | ChainV2[] memory chains = parser.parseChains(json);
31 | console.log("Adding", chains.length, "chains to", agreementAddress);
32 |
33 | vm.startBroadcast(pk);
34 | agreement.addChains(chains);
35 | vm.stopBroadcast();
36 |
37 | console.log("Added chains successfully");
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/.github/workflows/deploy-safeharbor.yaml:
--------------------------------------------------------------------------------
1 | name: Deploy Safe Harbor
2 |
3 | on:
4 | workflow_dispatch:
5 | inputs:
6 | rpc_url:
7 | description: "RPC URL for deployment"
8 | required: true
9 | type: string
10 | broadcast:
11 | description: "Include --broadcast flag"
12 | required: true
13 | default: false
14 | type: boolean
15 |
16 | jobs:
17 | deploy:
18 | name: Deploy Registry V2
19 | runs-on: ubuntu-latest
20 | if: ${{ github.event.inputs.rpc_url != '' }}
21 | steps:
22 | - name: Checkout repository
23 | uses: actions/checkout@v4
24 | with:
25 | token: ${{ secrets.GITHUB_TOKEN }}
26 | fetch-depth: 0
27 | submodules: recursive
28 |
29 | - name: Install Foundry
30 | uses: foundry-rs/foundry-toolchain@v1
31 |
32 | - name: Deploy Registry V2 (Simulation)
33 | run: forge script DeployRegistryV2 --rpc-url ${{ github.event.inputs.rpc_url }}
34 | working-directory: registry-contracts
35 | env:
36 | REGISTRY_DEPLOYER_PRIVATE_KEY: ${{ secrets.REGISTRY_DEPLOYER_PRIVATE_KEY }}
37 |
38 | - name: Deploy Registry V2 (Broadcast)
39 | if: ${{ github.event.inputs.broadcast == 'true' }}
40 | run: forge script DeployRegistryV2 --rpc-url ${{ github.event.inputs.rpc_url }} --broadcast --verify --retries 50
41 | working-directory: registry-contracts
42 | env:
43 | ETHERSCAN_API_KEY: ${{ secrets.ETHERSCAN_API_KEY }}
44 | REGISTRY_DEPLOYER_PRIVATE_KEY: ${{ secrets.REGISTRY_DEPLOYER_PRIVATE_KEY }}
45 |
--------------------------------------------------------------------------------
/assets/whitehat-logomark-blue.svg:
--------------------------------------------------------------------------------
1 |
17 |
--------------------------------------------------------------------------------
/documents/exhibits/d.tex:
--------------------------------------------------------------------------------
1 | \textit{TO BE ADAPTED AND INSERTED INTO THE TERMS OF SERVICE FOR ALL WEB APPLICATIONS RELEVANT TO USING THE PROTOCOL:}
2 |
3 | \subsubsection*{User Agreement to be Bound By Agreement, Consent to Attempted Eligible Funds Rescues and Payment of Bounties}\label{exhibit:d:user_agreement}
4 |
5 | The User hereby acknowledges and agrees to, and consents to be bound by the terms and conditions of, that certain Safe Harbor Agreement for Whitehats, adopted by the Protocol Community on [\_\_\_\_\_\_\_\_\_\_] (the "\textbf{Whitehat Agreement}"), available here [\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_], as a "User" and member of the "Protocol Community" thereunder. Without limiting the generality of the foregoing:
6 |
7 | \begin{itemize}
8 | \item the User hereby consents to Whitehats attempting Eligible Funds Rescues of any and all Tokens deposited into the Protocol by the User and the deduction of Bounties out of User's deposited Tokens to compensate Eligible Whitehats for successful Eligible Funds Rescues;
9 |
10 | \item the User acknowledges and agrees that Tokens may be lost, stolen, suffer diminished value, or become disabled or frozen in connection with attempts at Eligible Funds Rescues, and assumes all the risk of the foregoing;
11 |
12 | \item the User acknowledges and agrees that payment of the Bounty as a deduction from User's Tokens to an Eligible Whitehat may constitute a taxable disposition by the User of the deducted Tokens, and agrees to assume to all risk of such adverse tax treatment; and
13 |
14 | \item the User agrees to hold the other Protocol Community Members harmless from any loss, liability or other damages suffered by the User in connection with attempted Eligible Funds Exploits under the Whitehat Agreement.
15 | \end{itemize}
16 |
--------------------------------------------------------------------------------
/.github/workflows/latex.yaml:
--------------------------------------------------------------------------------
1 | name: Compile LaTeX
2 |
3 | permissions:
4 | contents: write
5 |
6 | on:
7 | push:
8 | branches: [main]
9 | paths:
10 | - "documents/agreement.tex"
11 | - "documents/summary.tex"
12 | - "documents/exhibits/**/*.tex"
13 |
14 | jobs:
15 | build:
16 | runs-on: ubuntu-latest
17 |
18 | steps:
19 | # 1. Checkout with write perms
20 | - name: Checkout repo
21 | uses: actions/checkout@v3
22 | with:
23 | token: ${{ secrets.GITHUB_TOKEN }}
24 | fetch-depth: 0
25 | persist-credentials: true
26 |
27 | # 2. Compile agreement.tex
28 | - name: Compile agreement.tex
29 | uses: dante-ev/latex-action@latest
30 | with:
31 | working_directory: documents
32 | root_file: agreement.tex
33 | compiler: latexmk
34 | args: >
35 | -pdf
36 | -latexoption="-file-line-error"
37 | -latexoption="-interaction=nonstopmode"
38 |
39 | # 3. Compile summary.tex
40 | - name: Compile summary.tex
41 | uses: dante-ev/latex-action@latest
42 | with:
43 | working_directory: documents
44 | root_file: summary.tex
45 | compiler: latexmk
46 | args: >
47 | -pdf
48 | -latexoption="-file-line-error"
49 | -latexoption="-interaction=nonstopmode"
50 |
51 | # 4. Commit & push both PDFs back to main
52 | - name: Commit generated PDFs
53 | uses: EndBug/add-and-commit@v9
54 | with:
55 | author_name: github-actions[bot]
56 | author_email: github-actions[bot]@users.noreply.github.com
57 | message: "ci: add compiled agreement.pdf & summary.pdf"
58 | add: "documents/*.pdf"
59 | push: true
60 |
--------------------------------------------------------------------------------
/registry-contracts/test/v2/AgreementV2Factory.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: UNLICENSED
2 | pragma solidity ^0.8.20;
3 |
4 | import {TestBase} from "forge-std/Test.sol";
5 | import {DSTest} from "ds-test/test.sol";
6 | import {console} from "forge-std/console.sol";
7 | import {Vm} from "forge-std/Vm.sol";
8 |
9 | import "../../src/v2/SafeHarborRegistryV2.sol";
10 | import "../../src/v2/AgreementFactoryV2.sol";
11 | import {AgreementV2} from "../../src/v2/AgreementV2.sol";
12 | import "../../src/v2/AgreementDetailsV2.sol";
13 | import {getMockAgreementDetails} from "./mock.sol";
14 |
15 | contract AgreementFactoryV2Test is TestBase, DSTest {
16 | SafeHarborRegistryV2 registry;
17 | AgreementFactoryV2 factory;
18 |
19 | address deployer;
20 | address protocol;
21 |
22 | AgreementDetailsV2 agreementDetails;
23 |
24 | function setUp() public {
25 | deployer = address(0xD3);
26 | protocol = address(0xAB);
27 |
28 | registry = new SafeHarborRegistryV2(deployer);
29 | factory = new AgreementFactoryV2();
30 |
31 | string[] memory validChains = new string[](2);
32 | validChains[0] = "eip155:1";
33 | validChains[1] = "eip155:2";
34 | vm.prank(deployer);
35 | registry.setValidChains(validChains);
36 |
37 | agreementDetails = getMockAgreementDetails("0xAABB");
38 | }
39 |
40 | function test_create() public {
41 | vm.prank(protocol);
42 | address agreementAddress = factory.create(agreementDetails, address(registry), protocol);
43 |
44 | AgreementV2 agreement = AgreementV2(agreementAddress);
45 | AgreementDetailsV2 memory storedDetails = agreement.getDetails();
46 | assertEq(keccak256(abi.encode(storedDetails)), keccak256(abi.encode(agreementDetails)));
47 |
48 | assertEq(agreement.owner(), protocol, "Agreement owner should be protocol");
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/registry-contracts/agreementDetailsV2.json:
--------------------------------------------------------------------------------
1 | {
2 | "agreementURI": "ipfs://QmExampleHashForSafeHarborAgreementDocument123456789",
3 | "bountyTerms": {
4 | "aggregateBountyCapUSD": 1000000,
5 | "bountyCapUSD": 1000000,
6 | "bountyPercentage": 10,
7 | "diligenceRequirements": "The protocol requires all eligible whitehats to undergo Know Your Customer (KYC) verification and be screened against global sanctions lists, including OFAC, UK, and EU regulations. This process ensures that all bounty recipients are compliant with legal and regulatory standards before qualifying for payment.",
8 | "identity": 2,
9 | "retainable": false
10 | },
11 | "chains": [
12 | {
13 | "accounts": [
14 | {
15 | "accountAddress": "0x1234567890123456789012345678901234567890",
16 | "childContractScope": 0
17 | },
18 | {
19 | "accountAddress": "0xAbCdEf1234567890123456789012345678901234",
20 | "childContractScope": 3
21 | }
22 | ],
23 | "assetRecoveryAddress": "0x9876543210987654321098765432109876543210",
24 | "id": "eip155:1"
25 | },
26 | {
27 | "accounts": [
28 | {
29 | "accountAddress": "HvNqQBTfoiksyvzGR5rrNAv46DjeNgGNMTB5YZpYh16W",
30 | "childContractScope": 3
31 | },
32 | {
33 | "accountAddress": "4JDyw4rd4Rm9rWgPG9jhXLcYoo3NYVoZDSxABpoD3ozQ",
34 | "childContractScope": 0
35 | }
36 | ],
37 | "assetRecoveryAddress": "CzYQ2kFnBxsNEt9Zy34vQ3n5fSDhvA4o4XaTnq1rLvyr",
38 | "id": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp"
39 | }
40 | ],
41 | "contact": [
42 | {
43 | "contact": "security@exampleprotocol.com",
44 | "name": "Example Protocol Security Team"
45 | },
46 | {
47 | "contact": "emergency-security@exampleprotocol.com",
48 | "name": "Emergency Response Team"
49 | }
50 | ],
51 | "protocolName": "Example Protocol"
52 | }
--------------------------------------------------------------------------------
/registry-contracts/script/v2/ChangeOwnerV2.s.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: UNLICENSED
2 | pragma solidity ^0.8.13;
3 |
4 | import {Script} from "forge-std/Script.sol";
5 | import {console} from "forge-std/console.sol";
6 |
7 | import {AgreementV2} from "../../src/v2/AgreementV2.sol";
8 |
9 | contract ChangeOwnerV2 is Script {
10 | /// @notice Entry via env vars: requires DEPLOYER_PRIVATE_KEY, AGREEMENT_ADDRESS, NEW_OWNER
11 | function run() public {
12 | uint256 pk = vm.envUint("DEPLOYER_PRIVATE_KEY");
13 | address agreementAddress = vm.envAddress("AGREEMENT_ADDRESS");
14 | address newOwner = vm.envAddress("NEW_OWNER");
15 | _transferOwnership(pk, agreementAddress, newOwner);
16 | }
17 |
18 | /// @notice Entry via CLI signature: forge script ... --sig 'run(address,address)'
19 | /// @dev Still reads DEPLOYER_PRIVATE_KEY from env for broadcasting
20 | function run(address agreementAddress, address newOwner) public {
21 | uint256 pk = vm.envUint("DEPLOYER_PRIVATE_KEY");
22 | _transferOwnership(pk, agreementAddress, newOwner);
23 | }
24 |
25 | function _transferOwnership(uint256 pk, address agreementAddress, address newOwner) internal {
26 | require(agreementAddress != address(0), "agreement address is zero");
27 | require(newOwner != address(0), "new owner is zero");
28 |
29 | AgreementV2 agreement = AgreementV2(agreementAddress);
30 | require(address(agreement).code.length > 0, "No contract at agreement address");
31 |
32 | address sender = vm.addr(pk);
33 | address currentOwner = agreement.owner();
34 | require(currentOwner == sender, "sender is not current owner");
35 | require(newOwner != currentOwner, "new owner equals current owner");
36 |
37 | console.log("Transferring ownership of agreement:");
38 | console.logAddress(agreementAddress);
39 | console.log("From:");
40 | console.logAddress(currentOwner);
41 | console.log("To:");
42 | console.logAddress(newOwner);
43 |
44 | vm.startBroadcast(pk);
45 | agreement.transferOwnership(newOwner);
46 | vm.stopBroadcast();
47 |
48 | console.log("Ownership transferred successfully");
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/registry-contracts/test/v1/SafeHarborRegistryTest.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: UNLICENSED
2 | pragma solidity ^0.8.20;
3 |
4 | /// @notice Imporing these packages directly due to naming conflicts between "Account" and "Chain" structs.
5 | import {TestBase} from "forge-std/Test.sol";
6 | import {DSTest} from "ds-test/test.sol";
7 | import {console} from "forge-std/console.sol";
8 | import {Vm} from "forge-std/Vm.sol";
9 | import "../../src/v1/SafeHarborRegistry.sol";
10 | import "./mock.sol";
11 |
12 | contract SafeHarborRegistryTest is TestBase, DSTest {
13 | SafeHarborRegistry registry;
14 | SafeHarborRegistry registryV2;
15 | AgreementDetailsV1 details;
16 |
17 | function setUp() public {
18 | registry = new SafeHarborRegistry(address(0));
19 | registryV2 = new SafeHarborRegistry(address(registry));
20 | details = getMockAgreementDetails(address(100));
21 | }
22 |
23 | function test_adoptSafeHarbor() public {
24 | address newDetails = 0x104fBc016F4bb334D775a19E8A6510109AC63E00;
25 | address entity = address(0xee);
26 |
27 | vm.expectEmit();
28 | emit SafeHarborRegistry.SafeHarborAdoption(entity, address(0), newDetails);
29 | vm.prank(entity);
30 | registry.adoptSafeHarbor(details);
31 | }
32 |
33 | function test_getDetails() public {
34 | address entity = address(0xee);
35 |
36 | vm.prank(entity);
37 | registry.adoptSafeHarbor(details);
38 | AgreementV1 agreement = AgreementV1(registry.getAgreement(entity));
39 | AgreementDetailsV1 memory gotDetails = agreement.getDetails();
40 | assertEq(registry.hash(details), registry.hash(gotDetails));
41 | }
42 |
43 | function test_getDetails_fallback() public {
44 | address entity = address(0xee);
45 |
46 | vm.prank(entity);
47 | registry.adoptSafeHarbor(details);
48 | AgreementV1 agreement = AgreementV1(registryV2.getAgreement(entity));
49 | AgreementDetailsV1 memory gotDetails = agreement.getDetails();
50 | assertEq(registry.hash(details), registry.hash(gotDetails));
51 | }
52 |
53 | function test_getDetails_missing() public {
54 | address entity = address(0xee);
55 |
56 | vm.expectRevert(SafeHarborRegistry.NoAgreement.selector);
57 | address agreement = registryV2.getAgreement(entity);
58 | assertEq(agreement, address(0));
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/registry-contracts/test/script/AdoptSafeHarbor.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: UNLICENSED
2 | pragma solidity ^0.8.20;
3 |
4 | /// @notice Importing these packages directly due to naming conflicts between "Account" and "Chain" structs.
5 | import {TestBase} from "forge-std/Test.sol";
6 | import {DSTest} from "ds-test/test.sol";
7 | import {console} from "forge-std/console.sol";
8 | import {Vm} from "forge-std/Vm.sol";
9 | import "../../src/v1/SafeHarborRegistry.sol";
10 | import "../../script/v1/AdoptSafeHarborV1.s.sol";
11 | import "../../script/v1/DeployRegistryV1.s.sol";
12 | import {getMockAgreementDetails, logAgreementDetails} from "../v1/mock.sol";
13 |
14 | contract AdoptSafeHarborV1Test is TestBase, DSTest {
15 | uint256 mockKey;
16 | address mockAddress;
17 | SafeHarborRegistry registry;
18 | string json;
19 |
20 | function setUp() public {
21 | // Deploy the safeharborRegistry
22 | string memory fakePrivateKey = "0xf0931a501a9b5fd5183d01f35526e5bc64d05d9d25d4005a8b1600ed6cd8d795";
23 | vm.setEnv("REGISTRY_DEPLOYER_PRIVATE_KEY", fakePrivateKey);
24 |
25 | DeployRegistryV1 script = new DeployRegistryV1();
26 | script.run();
27 |
28 | address fallbackRegistry = address(0);
29 | address registryAddr = script.getExpectedAddress(fallbackRegistry);
30 |
31 | mockKey = 0xA11;
32 | mockAddress = vm.addr(mockKey);
33 | registry = SafeHarborRegistry(registryAddr);
34 | json = vm.readFile("test/v1/mock.json");
35 | }
36 |
37 | function test_run() public {
38 | AdoptSafeHarborV1 script = new AdoptSafeHarborV1();
39 | script.adopt(mockKey, registry, json);
40 |
41 | // Check if the agreement was adopted
42 | address agreementAddr = registry.getAgreement(mockAddress);
43 | AgreementV1 agreement = AgreementV1(agreementAddr);
44 | AgreementDetailsV1 memory gotDetails = agreement.getDetails();
45 |
46 | console.logString("--------------------------GOT--------------------------");
47 | logAgreementDetails(gotDetails);
48 | console.logString("--------------------------WANT--------------------------");
49 | logAgreementDetails(getMockAgreementDetails(address(0x1111111111111111111111111111111111111111)));
50 |
51 | assertEq(
52 | registry.hash(getMockAgreementDetails(address(0x1111111111111111111111111111111111111111))),
53 | registry.hash(gotDetails)
54 | );
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/registry-contracts/src/v1/SafeHarborRegistry.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity ^0.8.20;
3 |
4 | import "./AgreementValidatorV1.sol";
5 |
6 | /// @title The Safe Harbor Registry. See www.securityalliance.org for details.
7 | contract SafeHarborRegistry is AgreementValidatorV1 {
8 | /// @notice A mapping which records the agreement details for a given governance/admin address.
9 | mapping(address entity => address details) private agreements;
10 |
11 | /// @notice The fallback registry.
12 | SafeHarborRegistry fallbackRegistry;
13 |
14 | /// ----- EVENTS -----
15 |
16 | /// @notice An event that records when an address either newly adopts the Safe Harbor, or alters its previous terms.
17 | event SafeHarborAdoption(address indexed entity, address oldDetails, address newDetails);
18 |
19 | /// ----- ERRORS -----
20 | error NoAgreement();
21 |
22 | /// ----- METHODS -----
23 | /// @notice Sets the factory and fallback registry addresses
24 | constructor(address _fallbackRegistry) {
25 | fallbackRegistry = SafeHarborRegistry(_fallbackRegistry);
26 | }
27 |
28 | function version() external pure returns (string memory) {
29 | return _version;
30 | }
31 |
32 | /// @notice Function that creates a new AgreementV1 contract and records it as an adoption by msg.sender.
33 | /// @param details The details of the agreement.
34 | function adoptSafeHarbor(AgreementDetailsV1 memory details) external {
35 | AgreementV1 agreementDetails = new AgreementV1(details);
36 | address agreementAddress = address(agreementDetails);
37 | address adopter = msg.sender;
38 |
39 | address oldDetails = agreements[adopter];
40 | agreements[adopter] = agreementAddress;
41 | emit SafeHarborAdoption(adopter, oldDetails, agreementAddress);
42 | }
43 |
44 | /// @notice Get the agreement address for the adopter. Recursively queries fallback registries.
45 | /// @param adopter The adopter to query.
46 | /// @return address The agreement address.
47 | function getAgreement(address adopter) external view returns (address) {
48 | address agreement = agreements[adopter];
49 |
50 | if (agreement != address(0)) {
51 | return agreement;
52 | }
53 |
54 | if (address(fallbackRegistry) != address(0)) {
55 | return fallbackRegistry.getAgreement(adopter);
56 | }
57 |
58 | revert NoAgreement();
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/js/test/eip712Test.js:
--------------------------------------------------------------------------------
1 | import {expect} from 'chai';
2 |
3 | import Web3 from "web3";
4 | import sigUtil from 'eth-sig-util';
5 |
6 | import { loadABI, loadAddress } from '../fs.js';
7 | import {domain} from '../domain.js';
8 | import {types, primaryType} from '../types.js';
9 |
10 | const artifactPath = '../registry-contracts/out/SafeHarborRegistry.sol/SafeHarborRegistry.json';
11 | const addressPath = '../registry-contracts/broadcast/SafeHarborRegistryDeploy.s.sol/31337/run-latest.json';
12 |
13 | // From forge created accounts, insecure
14 | const web3 = new Web3("http://localhost:8545");
15 | const accounts = await web3.eth.getAccounts();
16 | const signer = accounts[0];
17 | const signerPrivateKey = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
18 |
19 | describe("EIP712 Test", () => {
20 | it("Should successfully sign and validate signature", async () => {
21 | // Get registry
22 | const registryABI = await loadABI(artifactPath);
23 | const registryAddr = await loadAddress(addressPath);
24 | const registry = new web3.eth.Contract(registryABI, registryAddr);
25 |
26 | domain.verifyingContract = registryAddr;
27 |
28 | // Sign typedData
29 | const typedData = {
30 | types,
31 | domain,
32 | primaryType,
33 | message: value
34 | };
35 |
36 | const signature = sigUtil.signTypedData_v4(Buffer.from(signerPrivateKey.slice(2), 'hex'), { data: typedData });
37 |
38 | // Validate signature on smart contract
39 | const account = {
40 | accountAddress: signer,
41 | childContractScope: 0,
42 | signature
43 | };
44 |
45 | const isValid = await registry.methods.validateAccount(value, account).call();
46 | expect(isValid).to.be.true;
47 | })
48 | })
49 |
50 | // Mock value to be signed
51 | const value = {
52 | protocolName: "ExampleProtocol",
53 | contactDetails: [
54 | {
55 | name: "ExampleContact",
56 | contact: "contact@example.com"
57 | }
58 | ],
59 | chains: [
60 | {
61 | assetRecoveryAddress: "0xa83114a443da1cecefc50368531cace9f37fcccb",
62 | accounts: [
63 | {
64 | accountAddress: signer,
65 | childContractScope: 0,
66 | signature: "0x"
67 | }
68 | ],
69 | id: 1
70 | }
71 | ],
72 | bountyTerms: {
73 | bountyPercentage: 10,
74 | bountyCapUSD: 1000,
75 | retainable: true,
76 | identity: 0,
77 | diligenceRequirements: "Some requirements"
78 | },
79 | agreementURI: "https://example.com/agreement"
80 | };
--------------------------------------------------------------------------------
/registry-contracts/src/v1/SignatureValidator.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity ^0.8.20;
3 |
4 | import "./ERC1271.sol";
5 |
6 | contract SignatureValidator {
7 | /// @notice Returns the signer of a hash.
8 | /// @param hash The unsigned hash.
9 | /// @param signature The signature.
10 | /// @return signer The signer of the hash.
11 | function recoverSigner(bytes32 hash, bytes memory signature) internal pure returns (address signer) {
12 | uint8 v;
13 | bytes32 r;
14 | bytes32 s;
15 |
16 | assembly {
17 | r := mload(add(signature, 32))
18 | s := mload(add(signature, 64))
19 | v := byte(0, mload(add(signature, 96)))
20 | }
21 |
22 | signer = ecrecover(hash, v, r, s);
23 | require(signer != address(0), "Invalid signature");
24 | }
25 |
26 | /// @notice Returns whether an EOA signed a given hash.
27 | /// @param wantSigner The signer to check for.
28 | /// @param hash The hash that was signed.
29 | /// @param signature The signature.
30 | function isEOASignatureValid(address wantSigner, bytes32 hash, bytes memory signature)
31 | internal
32 | pure
33 | returns (bool)
34 | {
35 | address signer = recoverSigner(hash, signature);
36 | return signer == wantSigner;
37 | }
38 |
39 | /// @notice Returns whether a contract signed a given hash.
40 | /// @param wantSigner The signer to check for.
41 | /// @param hash The hash that was signed.
42 | /// @param signature The signature.
43 | function isContractSignatureValid(address wantSigner, bytes32 hash, bytes memory signature)
44 | internal
45 | view
46 | returns (bool)
47 | {
48 | bytes4 result = IERC1271(wantSigner).isValidSignature(hash, signature);
49 |
50 | // EIP-1271 magic value
51 | // https://eips.ethereum.org/EIPS/eip-1271
52 | return result == 0x1626ba7e;
53 | }
54 |
55 | /// @notice Returns whether an address is a contract.
56 | /// @param addr The address to check.
57 | function isContract(address addr) internal view returns (bool) {
58 | uint32 size;
59 | assembly {
60 | size := extcodesize(addr)
61 | }
62 | return (size > 0);
63 | }
64 |
65 | /// @notice Returns whether a signature is valid.
66 | /// @param wantSigner The signer to check for.
67 | /// @param hash The hash that was signed.
68 | /// @param signature The signature.
69 | function isSignatureValid(address wantSigner, bytes32 hash, bytes memory signature) public view returns (bool) {
70 | if (isContract(wantSigner)) {
71 | return isContractSignatureValid(wantSigner, hash, signature);
72 | } else {
73 | return isEOASignatureValid(wantSigner, hash, signature);
74 | }
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/registry-contracts/test/script/AdoptSafeHarborV2.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: UNLICENSED
2 | pragma solidity ^0.8.20;
3 |
4 | /// @notice Importing these packages directly due to naming conflicts between "Account" and "Chain" structs.
5 | import {TestBase} from "forge-std/Test.sol";
6 | import {DSTest} from "ds-test/test.sol";
7 | import {console} from "forge-std/console.sol";
8 | import {Vm} from "forge-std/Vm.sol";
9 | import "../../script/v2/AdoptSafeHarborV2.s.sol";
10 | import "../../script/v2/DeployRegistryV2.s.sol";
11 | import {getMockAgreementDetails, logAgreementDetails} from "../v2/mock.sol";
12 |
13 | contract AdoptSafeHarborV2Test is TestBase, DSTest {
14 | uint256 mockKey;
15 | address mockAddress;
16 | SafeHarborRegistryV2 registry;
17 | AgreementFactoryV2 factory;
18 | string json;
19 |
20 | function setUp() public {
21 | // Deploy the safeharborRegistry
22 | uint256 fakePrivateKey = 0xf0931a501a9b5fd5183d01f35526e5bc64d05d9d25d4005a8b1600ed6cd8d795;
23 | address deployerAddress = vm.addr(fakePrivateKey);
24 |
25 | string memory fakePrivateKeyHex = vm.toString(fakePrivateKey);
26 | vm.setEnv("REGISTRY_DEPLOYER_PRIVATE_KEY", fakePrivateKeyHex);
27 |
28 | DeployRegistryV2 script = new DeployRegistryV2();
29 | script.run();
30 |
31 | address registryAddr = script.getExpectedRegistryAddress(deployerAddress);
32 | address factoryAddr = script.getExpectedFactoryAddress();
33 |
34 | mockKey = 0xA11;
35 | mockAddress = vm.addr(mockKey);
36 | registry = SafeHarborRegistryV2(registryAddr);
37 | factory = AgreementFactoryV2(factoryAddr);
38 |
39 | string[] memory validChains = new string[](1);
40 | validChains[0] = "eip155:1";
41 |
42 | vm.prank(deployerAddress);
43 | registry.setValidChains(validChains);
44 |
45 | json = vm.readFile("test/v2/mock.json");
46 | }
47 |
48 | function test_adopt() public {
49 | AdoptSafeHarborV2 script = new AdoptSafeHarborV2();
50 | script.adopt(mockKey, registry, factory, json, mockAddress, true);
51 |
52 | // Check if the agreement was adopted
53 | address agreementAddr = registry.getAgreement(mockAddress);
54 | AgreementV2 agreement = AgreementV2(agreementAddr);
55 | AgreementDetailsV2 memory gotDetails = agreement.getDetails();
56 |
57 | console.logString("--------------------------GOT--------------------------");
58 | logAgreementDetails(gotDetails);
59 | console.logString("--------------------------WANT--------------------------");
60 | logAgreementDetails(getMockAgreementDetails("0x1111111111111111111111111111111111111111"));
61 |
62 | assertEq(
63 | keccak256(abi.encode(getMockAgreementDetails("0x1111111111111111111111111111111111111111"))),
64 | keccak256(abi.encode(gotDetails))
65 | );
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/registry-contracts/test/v1/mock.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: UNLICENSED
2 | pragma solidity ^0.8.20;
3 |
4 | import {console} from "forge-std/console.sol";
5 | import "../../src/v1/AgreementV1.sol";
6 |
7 | function getMockAgreementDetails(address accountAddress) pure returns (AgreementDetailsV1 memory mockDetails) {
8 | Account memory account =
9 | Account({accountAddress: accountAddress, childContractScope: ChildContractScope.All, signature: new bytes(0)});
10 |
11 | Chain memory chain = Chain({accounts: new Account[](1), assetRecoveryAddress: address(0x11), id: 1});
12 | chain.accounts[0] = account;
13 |
14 | Contact memory contact = Contact({name: "Test Name", contact: "test@mail.com"});
15 |
16 | BountyTerms memory bountyTerms = BountyTerms({
17 | bountyPercentage: 10,
18 | bountyCapUSD: 100,
19 | retainable: true,
20 | identity: IdentityRequirements.Anonymous,
21 | diligenceRequirements: "none"
22 | });
23 |
24 | mockDetails = AgreementDetailsV1({
25 | protocolName: "testProtocol",
26 | chains: new Chain[](1),
27 | contactDetails: new Contact[](1),
28 | bountyTerms: bountyTerms,
29 | agreementURI: "ipfs://testHash"
30 | });
31 | mockDetails.chains[0] = chain;
32 | mockDetails.contactDetails[0] = contact;
33 |
34 | return mockDetails;
35 | }
36 |
37 | function logAgreementDetails(AgreementDetailsV1 memory details) view {
38 | console.log("Agreement Details:");
39 | console.log("Protocol Name:", details.protocolName);
40 | console.log("Agreement URI:", details.agreementURI);
41 |
42 | // Print Contact Details
43 | console.log("Contact Details:");
44 | for (uint256 i = 0; i < details.contactDetails.length; i++) {
45 | console.log("Contact Name:", details.contactDetails[i].name);
46 | console.log("Contact Information:", details.contactDetails[i].contact);
47 | }
48 |
49 | // Print Chain Details
50 | console.log("Chain Details:");
51 | for (uint256 i = 0; i < details.chains.length; i++) {
52 | console.log(" Chain ID:", details.chains[i].id);
53 | console.log(" Asset Recovery Address:", details.chains[i].assetRecoveryAddress);
54 | console.log(" Number of Accounts in Scope:", details.chains[i].accounts.length);
55 |
56 | // Print Account Details
57 | for (uint256 j = 0; j < details.chains[i].accounts.length; j++) {
58 | console.log(" Account Address:", details.chains[i].accounts[j].accountAddress);
59 | console.log(" Child Contract Scope:", uint256(details.chains[i].accounts[j].childContractScope));
60 | console.log(" Signature: ", string(details.chains[i].accounts[j].signature));
61 | }
62 | }
63 |
64 | // Print Bounty Terms
65 | console.log("Bounty Percentage:", details.bountyTerms.bountyPercentage);
66 | console.log("Bounty Cap USD:", details.bountyTerms.bountyCapUSD);
67 | console.log("Is Retainable:", details.bountyTerms.retainable);
68 | console.log("Identity Requirement:", uint256(details.bountyTerms.identity));
69 | console.log("Diligence Requirements:", details.bountyTerms.diligenceRequirements);
70 | }
71 |
--------------------------------------------------------------------------------
/documents/FAQ.md:
--------------------------------------------------------------------------------
1 | ## What is the Safe Harbor Initiative?
2 |
3 | The Safe Harbor initiative is a legal framework for protocols to allow the rescue of funds being actively exploited. Essentially, this framework aims to give legal protection and financial incentives to well-intentioned whitehats who are capable of rescuing funds that are being stolen.
4 |
5 |
6 | ## Who created the Safe Harbor Initiative?
7 |
8 | The Safe Harbor initiative was created by the Security Alliance - a team of professionals spearheading public good projects that bolster the security of the Web3 community. See [securityalliance.org](https://securityalliance.org/) for more info.
9 |
10 |
11 | ## What are the components?
12 |
13 | The main legal document is the [“Whitehat Safe Harbor Agreement”](../documents/agreement.pdf). This legal document contains the following exhibits:
14 |
15 | - **Certain Defined Terms (Exhibit A):** defines relevant terminology
16 | - **DAO Adoption Procedure (Exhibit B):** describes how a protocol should initiate adoption
17 | - **Security Team Adoption Procedures (Exhibit C):** for acknowledgment by dev/security team
18 | - **User Adoption Procedures (Exhibit D):** user acknowledgment added to front-end ToS
19 | - **Whitehat Risk Disclosures (Exhibit E):** lists the risks associated with funds rescue
20 | - **Adoption Form (Exhibit F):** completed during the adoption procedure
21 | - **Summary (Exhibit G)**
22 | - **Protocol FAQ (Exhibit H)**
23 |
24 | A helper document called the [Whitehat Safe Harbor Agreement - Summary”](../documents/summary.pdf) also exists to summarize the main ideas.
25 |
26 | The on-chain components of the Safe Harbor initiative exist within the [registry-contracts/](../registry-contracts/) directory in the safe-harbor GitHub repo.
27 |
28 |
29 | ## How does this differ from a bug bounty program?
30 |
31 | In bug bounty programs, whitehats identify and report security vulnerabilities that are not yet publicly known. This allows for a more controlled response, as the information is initially shared with a limited audience, reducing immediate risk.
32 |
33 | With the Safe Harbor Initiative, whitehat intervention is permitted only after an exploit has been attempted by a separate malicious actor. This scenario requires a more immediate and urgent response. The Safe Harbor agreement preemptively grants whitehats the authorization to act in these circumstances, ensuring that they can address immediate threats without the delay of communicating with the protocol.
34 |
35 |
36 | ## What are the requirements for a whitehat to participate?
37 |
38 | To assist in a rescue, a whitehat must have sufficient experience in blockchain security to perform the rescue competently. While there is no formal standard, they should have some background experience in software engineering, security, and/or blockchain auditing. They must also be free from OFAC sanctions and not involved in legal issues related to any other blockchain exploits.
39 |
40 |
41 | ## Can a protocol impose KYC requirements on rescue payouts?
42 |
43 | Yes, protocols have the option to implement KYC requirements in their agreement. For a list of choices a protocol can make during adoption, refer to **Exhibit F (Adoption Form)**.
--------------------------------------------------------------------------------
/registry-contracts/src/v2/AgreementDetailsV2.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity ^0.8.20;
3 |
4 | /// @notice Struct that contains the details of the agreement.
5 | struct AgreementDetailsV2 {
6 | // The name of the protocol adopting the agreement.
7 | string protocolName;
8 | // The contact details (required for pre-notifying).
9 | Contact[] contactDetails;
10 | // The scope and recovery address by chain.
11 | Chain[] chains;
12 | // The terms of the agreement.
13 | BountyTerms bountyTerms;
14 | // IPFS hash of the actual agreement document, which confirms all terms.
15 | string agreementURI;
16 | }
17 |
18 | /// @notice Struct that contains the contact details of the agreement.
19 | struct Contact {
20 | string name;
21 | // This person's contact details (email, phone, telegram handle, etc.)
22 | string contact;
23 | }
24 |
25 | /// @notice Struct that contains the details of an agreement by chain.
26 | struct Chain {
27 | // The address to which recovered assets will be sent.
28 | string assetRecoveryAddress;
29 | // The accounts in scope for the agreement.
30 | Account[] accounts;
31 | // The CAIP-2 chain ID. Please refer to the CAIP-2 standard for more details.
32 | // https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-2.md
33 | string caip2ChainId;
34 | }
35 |
36 | /// @notice Struct that contains the details of an account in an agreement.
37 | struct Account {
38 | // The address of the account (EOA or smart contract).
39 | string accountAddress;
40 | // The scope of child contracts included in the agreement.
41 | ChildContractScope childContractScope;
42 | }
43 |
44 | /// @notice Enum that defines the inclusion of child contracts in an agreement.
45 | enum ChildContractScope {
46 | // No child contracts are included.
47 | None,
48 | // Only child contracts that were created before the time of this agreement are included.
49 | ExistingOnly,
50 | // All child contracts, both existing and new, are included.
51 | All,
52 | // Only child contracts that were created after the time of this agreement are included.
53 | FutureOnly
54 | }
55 |
56 | /// @notice Struct that contains the terms of the bounty for the agreement.
57 | struct BountyTerms {
58 | // Percentage of the recovered funds a Whitehat receives as their bounty (0-100).
59 | uint256 bountyPercentage;
60 | // The maximum bounty in USD.
61 | uint256 bountyCapUSD;
62 | // Whether the whitehat can retain their bounty or must return all funds to
63 | // the asset recovery address.
64 | bool retainable;
65 | // The identity verification requirements on the whitehat.
66 | IdentityRequirements identity;
67 | // The diligence requirements placed on eligible whitehats. Only applicable for Named whitehats.
68 | string diligenceRequirements;
69 | // Optional. Caps the total USD value of bounties paid across all whitehats for a single exploit.
70 | // If set to 0, no aggregate cap applies and each whitehat may receive up to bountyCapUSD individually.
71 | uint256 aggregateBountyCapUSD;
72 | }
73 |
74 | /// @notice Whitehat identity verification requirements.
75 | enum IdentityRequirements {
76 | // The whitehat will be subject to no KYC requirements.
77 | Anonymous,
78 | // The whitehat must provide a pseudonym.
79 | Pseudonymous,
80 | // The whitehat must confirm their legal name.
81 | Named
82 | }
83 |
--------------------------------------------------------------------------------
/registry-contracts/script/v1/DeployRegistryV1.s.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: UNLICENSED
2 | pragma solidity ^0.8.13;
3 |
4 | import {Script} from "forge-std/Script.sol";
5 | import {console} from "forge-std/console.sol";
6 | import {SafeHarborRegistry} from "../../src/v1/SafeHarborRegistry.sol";
7 |
8 | contract DeployRegistryV1 is Script {
9 | // This is a create2 factory deployed by a one-time-use-account as described here:
10 | // https://github.com/Arachnid/deterministic-deployment-proxy. As a result, this factory
11 | // exists (or can exist) on any EVM compatible chain, and gives us a guaranteed way to deploy
12 | // the registry to a deterministic address across all chains. This is used by default in foundry
13 | // when we specify a salt in a contract creation.
14 | address constant DETERMINISTIC_CREATE2_FACTORY = 0x4e59b44847b379578588920cA78FbF26c0B4956C;
15 |
16 | // This could have been any value, but we choose zero.
17 | bytes32 constant DETERMINISTIC_DEPLOY_SALT = bytes32(0);
18 |
19 | // This is the address of the fallback registry that has already been deployed.
20 | // Set this to the zero address if no fallback registry exists.
21 | address fallbackRegistry = address(0);
22 |
23 | function run() public {
24 | require(
25 | DETERMINISTIC_CREATE2_FACTORY.code.length != 0,
26 | "Create2 factory not deployed yet, see https://github.com/Arachnid/deterministic-deployment-proxy."
27 | );
28 |
29 | address registryAddress = getExpectedAddress(fallbackRegistry);
30 | require(registryAddress.code.length == 0, "Registry already deployed, nothing left to do.");
31 |
32 | uint256 deployerPrivateKey = vm.envUint("REGISTRY_DEPLOYER_PRIVATE_KEY");
33 | vm.startBroadcast(deployerPrivateKey);
34 |
35 | SafeHarborRegistry registry = new SafeHarborRegistry{salt: DETERMINISTIC_DEPLOY_SALT}(fallbackRegistry);
36 | address deployedRegistryAddress = address(registry);
37 |
38 | require(
39 | deployedRegistryAddress == registryAddress,
40 | "Deployed to unexpected address. Check that Foundry is using the correct create2 factory."
41 | );
42 |
43 | require(
44 | deployedRegistryAddress.code.length != 0,
45 | "Registry deployment failed. Check that Foundry is using the correct create2 factory."
46 | );
47 |
48 | console.log("SafeHarborRegistry deployed to:");
49 | console.logAddress(deployedRegistryAddress);
50 |
51 | vm.stopBroadcast();
52 | }
53 |
54 | // Computes the address which the registry will be deployed to, assuming the correct create2 factory
55 | // and salt are used.
56 | function getExpectedAddress(address _fallbackRegistry) public pure returns (address) {
57 | return address(
58 | uint160(
59 | uint256(
60 | keccak256(
61 | abi.encodePacked(
62 | bytes1(0xff),
63 | DETERMINISTIC_CREATE2_FACTORY,
64 | DETERMINISTIC_DEPLOY_SALT,
65 | keccak256(
66 | abi.encodePacked(type(SafeHarborRegistry).creationCode, abi.encode(_fallbackRegistry))
67 | )
68 | )
69 | )
70 | )
71 | )
72 | );
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/registry-contracts/test/v1/SignatureValidatorTest.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: UNLICENSED
2 | pragma solidity ^0.8.20;
3 |
4 | /// @notice Imporing these packages directly due to naming conflicts between "Account" and "Chain" structs.
5 | import {TestBase} from "forge-std/Test.sol";
6 | import {DSTest} from "ds-test/test.sol";
7 | import {console} from "forge-std/console.sol";
8 | import {Vm} from "forge-std/Vm.sol";
9 | import "../../src/v1/SignatureValidator.sol";
10 |
11 | contract SignatureValidatorTest is TestBase, DSTest {
12 | SignatureValidator validator;
13 |
14 | function setUp() public {
15 | validator = new SignatureValidator();
16 | }
17 |
18 | /// @notice Test isSignatureValid function with a valid EOA.
19 | function test_isSignatureValid_EOA() public {
20 | uint256 key = 100;
21 |
22 | bytes32 hash = keccak256(abi.encodePacked("Safe Harbor"));
23 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(key, hash);
24 | bytes memory signature = abi.encodePacked(r, s, v);
25 |
26 | bool isValid = validator.isSignatureValid(vm.addr(key), hash, signature);
27 | assertTrue(isValid);
28 | }
29 |
30 | /// @notice Test isSignatureValid function with an invalid EOA.
31 | function test_isSignatureValid_EOA_invalid() public {
32 | uint256 key = 100;
33 | address invalidAddress = address(0x11);
34 |
35 | bytes32 hash = keccak256(abi.encodePacked("Safe Harbor"));
36 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(key, hash);
37 | bytes memory signature = abi.encodePacked(r, s, v);
38 |
39 | bool isValid = validator.isSignatureValid(invalidAddress, hash, signature);
40 | assertTrue(!isValid);
41 | }
42 |
43 | /// @notice Test isSignatureValid function with a valid ERC1271 contract.
44 | function test_isSignatureValid_contract() public {
45 | uint256 key = 100;
46 |
47 | bytes32 hash = keccak256(abi.encodePacked("Safe Harbor"));
48 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(key, hash);
49 | bytes memory signature = abi.encodePacked(r, s, v);
50 |
51 | FakeERC1271 fakeContract = new FakeERC1271(hash, signature);
52 | bool isValid = validator.isSignatureValid(address(fakeContract), hash, signature);
53 |
54 | assertTrue(isValid);
55 | }
56 |
57 | /// @notice Test isSignatureValid function with an invalid valid ERC1271 contract.
58 | function test_isSignatureValid_contract_invalid() public {
59 | uint256 key = 100;
60 | bytes memory fakesignature = abi.encodePacked(uint256(1));
61 |
62 | bytes32 hash = keccak256(abi.encodePacked("Safe Harbor"));
63 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(key, hash);
64 | bytes memory signature = abi.encodePacked(r, s, v);
65 |
66 | FakeERC1271 fakeContract = new FakeERC1271(hash, fakesignature);
67 | bool isValid = validator.isSignatureValid(address(fakeContract), hash, signature);
68 |
69 | assertTrue(!isValid);
70 | }
71 | }
72 |
73 | contract FakeERC1271 {
74 | bytes32 wantHash;
75 | bytes wantSignature;
76 |
77 | constructor(bytes32 _wantHash, bytes memory _wantSignature) {
78 | wantHash = _wantHash;
79 | wantSignature = _wantSignature;
80 | }
81 |
82 | function isValidSignature(bytes32 hash, bytes memory signature) external view returns (bytes4) {
83 | if (hash == wantHash && keccak256(signature) == keccak256(wantSignature)) {
84 | return 0x1626ba7e;
85 | }
86 | return 0x0;
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/registry-contracts/script/v2/SetChains.s.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: UNLICENSED
2 | pragma solidity ^0.8.13;
3 |
4 | import {Script} from "forge-std/Script.sol";
5 | import {console} from "forge-std/console.sol";
6 | import {SafeHarborRegistryV2} from "../../src/v2/SafeHarborRegistryV2.sol";
7 |
8 | contract SetChains is Script {
9 | address constant REGISTRY_ADDRESS = 0x1eaCD100B0546E433fbf4d773109cAD482c34686;
10 |
11 | function run() public {
12 | SafeHarborRegistryV2 registry = SafeHarborRegistryV2(REGISTRY_ADDRESS);
13 | require(address(registry).code.length > 0, "No contract exists at the registry address.");
14 |
15 | uint256 deployerPrivateKey = vm.envUint("REGISTRY_DEPLOYER_PRIVATE_KEY");
16 |
17 | // CAIP-2 chain IDs for various chains
18 | string[] memory caip2ChainIds = new string[](52);
19 | caip2ChainIds[0] = "eip155:1"; // Ethereum
20 | caip2ChainIds[1] = "eip155:56"; // BSC
21 | caip2ChainIds[2] = "eip155:42161"; // Arbitrum
22 | caip2ChainIds[3] = "eip155:137"; // Polygon
23 | caip2ChainIds[4] = "eip155:8453"; // Base
24 | caip2ChainIds[5] = "eip155:43114"; // Avalanche
25 | caip2ChainIds[6] = "eip155:10"; // Optimism
26 | caip2ChainIds[7] = "tron:mainnet"; // Tron (mainnet)
27 | caip2ChainIds[8] = "eip155:1284"; // Moonbeam
28 | caip2ChainIds[9] = "eip155:1285"; // Moonriver
29 | caip2ChainIds[10] = "eip155:252"; // Fraxtal
30 | caip2ChainIds[11] = "eip155:100"; // Gnosis
31 | caip2ChainIds[12] = "eip155:34443"; // Mode
32 | caip2ChainIds[13] = "eip155:1101"; // Polygon ZkEVM
33 | caip2ChainIds[14] = "eip155:146"; // Sonic
34 | caip2ChainIds[15] = "eip155:81457"; // Blast
35 | caip2ChainIds[16] = "eip155:288"; // Boba
36 | caip2ChainIds[17] = "eip155:42220"; // Celo
37 | caip2ChainIds[18] = "eip155:314"; // Filecoin
38 | caip2ChainIds[19] = "eip155:59144"; // Linea
39 | caip2ChainIds[20] = "eip155:169"; // Manta Pacific
40 | caip2ChainIds[21] = "eip155:5000"; // Mantle
41 | caip2ChainIds[22] = "eip155:690"; // Berachain
42 | caip2ChainIds[23] = "eip155:30"; // Unichain
43 | caip2ChainIds[24] = "eip155:534352"; // Scroll
44 | caip2ChainIds[25] = "eip155:1329"; // Sei Network
45 | caip2ChainIds[26] = "eip155:167000"; // Taiko Alethia
46 | caip2ChainIds[27] = "eip155:480"; // World Chain
47 | caip2ChainIds[28] = "eip155:324"; // zkSync Mainnet
48 | caip2ChainIds[29] = "eip155:7777777"; // Zora
49 | caip2ChainIds[30] = "eip155:204"; // opBNB Mainnet
50 | caip2ChainIds[31] = "eip155:1088"; // Metis Andromeda Mainnet
51 | caip2ChainIds[32] = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp"; // Solana Mainnet
52 | caip2ChainIds[33] = "stellar:pubnet"; // Stellar Mainnet
53 | caip2ChainIds[34] = "bip122:000000000019d6689c085ae165831e93"; // Bitcoin Mainnet
54 | caip2ChainIds[35] = "eip155:999"; // HyperEVM
55 | caip2ChainIds[36] = "eip155:25"; // Cronos
56 | caip2ChainIds[37] = "eip155:1116"; // CORE
57 | caip2ChainIds[38] = "eip155:747474"; // Katana
58 | caip2ChainIds[39] = "eip155:369"; // Pulsechain
59 | caip2ChainIds[40] = "eip155:30"; // Rootstock
60 | caip2ChainIds[41] = "eip155:81457"; // Blast
61 | caip2ChainIds[42] = "eip155:2222"; // Kava
62 | caip2ChainIds[43] = "eip155:8217"; // Kaia
63 | caip2ChainIds[44] = "eip155:200901"; // Bitlayer
64 | caip2ChainIds[45] = "eip155:60808"; // Bob
65 | caip2ChainIds[46] = "eip155:98866"; // Plume
66 | caip2ChainIds[47] = "eip155:43111"; // Hemi
67 | caip2ChainIds[48] = "eip155:14"; // Flare
68 | caip2ChainIds[49] = "eip155:1868"; // Soneium
69 | caip2ChainIds[50] = "eip155:295"; // Hedera
70 | caip2ChainIds[51] = "eip155:9745"; // Plasma
71 |
72 | vm.broadcast(deployerPrivateKey);
73 | registry.setValidChains(caip2ChainIds);
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/registry-contracts/test/v1/AgreementValidatorV1Test.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: UNLICENSED
2 | pragma solidity ^0.8.20;
3 |
4 | /// @notice Imporing these packages directly due to naming conflicts between "Account" and "Chain" structs.
5 | import {TestBase} from "forge-std/Test.sol";
6 | import {DSTest} from "ds-test/test.sol";
7 | import {console} from "forge-std/console.sol";
8 | import {Vm} from "forge-std/Vm.sol";
9 | import "../../src/v1/SafeHarborRegistry.sol";
10 | import "../../src/v1/AgreementV1.sol";
11 | import "./mock.sol";
12 |
13 | contract AgreementValidatorV1Test is TestBase, DSTest {
14 | AgreementValidatorV1 validator;
15 | AgreementDetailsV1 details;
16 |
17 | uint256 mockKey;
18 | address mockAddress;
19 |
20 | function setUp() public {
21 | mockKey = 0xA11CE;
22 | mockAddress = vm.addr(mockKey);
23 |
24 | validator = new AgreementValidatorV1();
25 | details = getMockAgreementDetails(mockAddress);
26 | }
27 |
28 | function assertEq(AgreementDetailsV1 memory expected, AgreementDetailsV1 memory actual) public {
29 | bytes memory expectedBytes = abi.encode(expected);
30 | bytes memory actualBytes = abi.encode(actual);
31 |
32 | assertEq0(expectedBytes, actualBytes);
33 | }
34 |
35 | function test_validateAccount() public {
36 | bytes32 digest = validator.getTypedDataHash(details);
37 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(mockKey, digest);
38 | bytes memory signature = abi.encodePacked(r, s, v);
39 |
40 | details.chains[0].accounts[0].signature = signature;
41 |
42 | bool isValid = validator.validateAccount(details, details.chains[0].accounts[0]);
43 | assertTrue(isValid);
44 | }
45 |
46 | function test_validateAccount_invalid() public {
47 | uint256 fakeKey = 200;
48 |
49 | bytes32 digest = validator.getTypedDataHash(details);
50 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(fakeKey, digest);
51 | bytes memory signature = abi.encodePacked(r, s, v);
52 |
53 | details.chains[0].accounts[0].signature = signature;
54 |
55 | bool isValid = validator.validateAccount(details, details.chains[0].accounts[0]);
56 | assertTrue(!isValid);
57 | }
58 |
59 | function test_validateAccountByAddress() public {
60 | //* Deploy a new AgreementV1
61 | AgreementV1 newAgreement = new AgreementV1(details);
62 | address newAgreementAddr = address(newAgreement);
63 |
64 | //* Sign the details with the mock key
65 | bytes32 digest = validator.getTypedDataHash(details);
66 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(mockKey, digest);
67 | bytes memory signature = abi.encodePacked(r, s, v);
68 |
69 | // Update the account's signature in the details
70 | details.chains[0].accounts[0].signature = signature;
71 |
72 | //* Validate the signature using validateAccountByAddress
73 | bool isValid = validator.validateAccountByAddress(newAgreementAddr, details.chains[0].accounts[0]);
74 |
75 | //* Assert that the validation is successful
76 | assertTrue(isValid);
77 | }
78 |
79 | function test_validateAccountByAddress_invalid() public {
80 | //* Deploy a new AgreementV1
81 | AgreementV1 newAgreement = new AgreementV1(details);
82 | address newAgreementAddr = address(newAgreement);
83 |
84 | //* Sign the details with a fake key (to simulate an invalid signature)
85 | uint256 fakeKey = 200;
86 | bytes32 digest = validator.getTypedDataHash(details);
87 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(fakeKey, digest);
88 | bytes memory signature = abi.encodePacked(r, s, v);
89 |
90 | // Update the account's signature in the details with an invalid signature
91 | details.chains[0].accounts[0].signature = signature;
92 |
93 | //* Validate the signature using validateAccountByAddress
94 | bool isValid = validator.validateAccountByAddress(newAgreementAddr, details.chains[0].accounts[0]);
95 |
96 | //* Assert that the validation fails
97 | assertTrue(!isValid);
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/registry-contracts/src/v1/AgreementV1.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity ^0.8.20;
3 |
4 | string constant _version = "1.0.0";
5 |
6 | /// @notice Contract that contains the AgreementDetails that will be deployed by the Agreement Factory.
7 | contract AgreementV1 {
8 | /// @notice The details of the agreement.
9 | AgreementDetailsV1 private details;
10 |
11 | /// @notice Constructor that sets the details of the agreement.
12 | /// @param _details The details of the agreement.
13 | constructor(AgreementDetailsV1 memory _details) {
14 | details = _details;
15 | }
16 |
17 | function version() external pure returns (string memory) {
18 | return _version;
19 | }
20 |
21 | /// @notice Function that returns the details of the agreement.
22 | /// @dev You need a view function, else it won't convert storage to memory automatically for the nested structs.
23 | /// @return AgreementDetailsV1 The details of the agreement.
24 | function getDetails() external view returns (AgreementDetailsV1 memory) {
25 | return details;
26 | }
27 | }
28 |
29 | /// @notice Struct that contains the details of the agreement.
30 | struct AgreementDetailsV1 {
31 | // The name of the protocol adopting the agreement.
32 | string protocolName;
33 | // The contact details (required for pre-notifying).
34 | Contact[] contactDetails;
35 | // The scope and recovery address by chain.
36 | Chain[] chains;
37 | // The terms of the agreement.
38 | BountyTerms bountyTerms;
39 | // IPFS hash of the actual agreement document, which confirms all terms.
40 | string agreementURI;
41 | }
42 |
43 | /// @notice Struct that contains the contact details of the agreement.
44 | struct Contact {
45 | string name;
46 | // This person's contact details (email, phone, telegram handle, etc.)
47 | string contact;
48 | }
49 |
50 | /// @notice Struct that contains the details of an agreement by chain.
51 | struct Chain {
52 | // The address to which recovered assets will be sent.
53 | address assetRecoveryAddress;
54 | // The accounts in scope for the agreement.
55 | Account[] accounts;
56 | // The chain ID.
57 | uint256 id;
58 | }
59 |
60 | /// @notice Struct that contains the details of an account in an agreement.
61 | struct Account {
62 | // The address of the account (EOA or smart contract).
63 | address accountAddress;
64 | // The scope of child contracts included in the agreement.
65 | ChildContractScope childContractScope;
66 | // The signature of the account. Optionally used to verify that this account has accepted this agreement.
67 | // Instructions for generating this signature may be found in the [README](../README.md).
68 | bytes signature;
69 | }
70 |
71 | /// @notice Enum that defines the inclusion of child contracts in an agreement.
72 | enum ChildContractScope {
73 | // No child contracts are included.
74 | None,
75 | // Only child contracts that exist at the time of this agreement are included.
76 | ExistingOnly,
77 | // All child contracts, both existing and new, are included.
78 | All
79 | }
80 |
81 | /// @notice Struct that contains the terms of the bounty for the agreement.
82 | struct BountyTerms {
83 | // Percentage of the recovered funds a Whitehat receives as their bounty (0-100).
84 | uint256 bountyPercentage;
85 | // The maximum bounty in USD.
86 | uint256 bountyCapUSD;
87 | // Whether the whitehat can retain their bounty or must return all funds to
88 | // the asset recovery address.
89 | bool retainable;
90 | // The identity verification requirements on the whitehat.
91 | IdentityRequirements identity;
92 | // The diligence requirements placed on eligible whitehats. Only applicable for Named whitehats.
93 | string diligenceRequirements;
94 | }
95 |
96 | /// @notice Whitehat identity verification requirements.
97 | enum IdentityRequirements {
98 | // The whitehat will be subject to no KYC requirements.
99 | Anonymous,
100 | // The whitehat must provide a pseudonym.
101 | Pseudonymous,
102 | // The whitehat must confirm their legal name.
103 | Named
104 | }
105 |
--------------------------------------------------------------------------------
/registry-contracts/test/v2/mock.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: UNLICENSED
2 | pragma solidity ^0.8.20;
3 |
4 | import {console} from "forge-std/console.sol";
5 | import "../../src/v2/AgreementV2.sol";
6 | import "../../src/v2/AgreementDetailsV2.sol";
7 |
8 | function getMockAgreementDetails(string memory accountAddress) pure returns (AgreementDetailsV2 memory mockDetails) {
9 | Account memory account = Account({accountAddress: accountAddress, childContractScope: ChildContractScope.All});
10 |
11 | Chain memory chain = Chain({
12 | accounts: new Account[](1),
13 | assetRecoveryAddress: "0x0000000000000000000000000000000000000022",
14 | caip2ChainId: "eip155:1"
15 | });
16 | chain.accounts[0] = account;
17 |
18 | Contact memory contact = Contact({name: "Test Name V2", contact: "test@mail.com"});
19 |
20 | BountyTerms memory bountyTerms = BountyTerms({
21 | bountyPercentage: 10,
22 | bountyCapUSD: 100,
23 | retainable: false,
24 | identity: IdentityRequirements.Anonymous,
25 | diligenceRequirements: "none",
26 | aggregateBountyCapUSD: 1000
27 | });
28 |
29 | mockDetails = AgreementDetailsV2({
30 | protocolName: "testProtocolV2",
31 | chains: new Chain[](1),
32 | contactDetails: new Contact[](1),
33 | bountyTerms: bountyTerms,
34 | agreementURI: "ipfs://testHash"
35 | });
36 | mockDetails.chains[0] = chain;
37 | mockDetails.contactDetails[0] = contact;
38 |
39 | return mockDetails;
40 | }
41 |
42 | function logAgreementDetails(AgreementDetailsV2 memory details) view {
43 | string[3] memory identityRequirements = ["Anonymous", "Pseudonymous", "Named"];
44 | string[4] memory childContractScopes = ["None", "ExistingOnly", "All", "FutureOnly"];
45 |
46 | console.log("Agreement Details:");
47 | console.log("Protocol Name:", details.protocolName);
48 | console.log("Agreement URI:", details.agreementURI);
49 |
50 | // Print Contact Details
51 | console.log("Contact Details:");
52 | for (uint256 i = 0; i < details.contactDetails.length; i++) {
53 | console.log("Contact Name:", details.contactDetails[i].name);
54 | console.log("Contact Information:", details.contactDetails[i].contact);
55 | }
56 |
57 | // Print Chain Details
58 | console.log("Chain Details:");
59 | for (uint256 i = 0; i < details.chains.length; i++) {
60 | console.log(" Chain ID:", details.chains[i].caip2ChainId);
61 | console.log(" Asset Recovery Address:", details.chains[i].assetRecoveryAddress);
62 | console.log(" Number of Accounts in Scope:", toString(details.chains[i].accounts.length));
63 |
64 | // Print Account Details
65 | for (uint256 j = 0; j < details.chains[i].accounts.length; j++) {
66 | console.log(" Account Address:", details.chains[i].accounts[j].accountAddress);
67 | console.log(
68 | " Child Contract Scope:",
69 | childContractScopes[uint256(details.chains[i].accounts[j].childContractScope)]
70 | );
71 | }
72 | }
73 |
74 | // Print Bounty Terms
75 | console.log("Bounty Percentage:", toString(uint256(details.bountyTerms.bountyPercentage)));
76 | console.log("Bounty Cap USD:", toString(details.bountyTerms.bountyCapUSD));
77 | console.log("Aggregate Bounty Cap USD:", toString(details.bountyTerms.aggregateBountyCapUSD));
78 | console.log("Is Retainable:", details.bountyTerms.retainable ? "Yes" : "No");
79 | console.log("Identity Requirement:", identityRequirements[uint256(details.bountyTerms.identity)]);
80 | console.log("Diligence Requirements:", details.bountyTerms.diligenceRequirements);
81 | }
82 |
83 | function toString(uint256 value) pure returns (string memory) {
84 | if (value == 0) {
85 | return "0";
86 | }
87 |
88 | uint256 temp = value;
89 | uint256 digits;
90 |
91 | while (temp != 0) {
92 | digits++;
93 | temp /= 10;
94 | }
95 |
96 | bytes memory buffer = new bytes(digits);
97 |
98 | while (value != 0) {
99 | digits--;
100 | buffer[digits] = bytes1(uint8(48 + (value % 10)));
101 | value /= 10;
102 | }
103 |
104 | return string(buffer);
105 | }
106 |
--------------------------------------------------------------------------------
/documents/exhibits/b.tex:
--------------------------------------------------------------------------------
1 | \textit{Note any Protocol-specific DAO Proposal procedures, such as sentiment checks, pre-proposal audit, etc.}
2 |
3 | DAO Proposal Components:
4 |
5 | \begin{enumerate}
6 | \item \textbf{Title} - Post each proposal with a clear title around its objective, matching or referencing a unique identifier of the proposal that was submitted on-chain or will be submitted on-chain (for example, IPFS hash of pinned text for a prospective proposal, or transaction hash of a submitted proposal), and should follow any applicable ordering/numbering/categorization of the Protocol DAO.
7 |
8 | \textit{Ex.} [Proposal No. \_\_] - Adopt Safe Harbor Agreement for Whitehats
9 |
10 | \item \textbf{Overview} - Delineate the objectives of the proposal and what specific actions are being enacted (if on-chain governance) and suggested (for off-chain signaled actions). The summary should specify on-chain target contracts and methods, and off-chain agents/designees, and describe the motivation behind the proposal, including but not limited to the problem(s) it solves and the value it adds to the Protocol and Protocol Community.
11 |
12 | \textit{Ex.} The Security Alliance has prepared a Safe Harbor Agreement for Whitehats (the "Agreement") to incentivize and give comfort to whitehats rescuing digital assets from active exploits of decentralized technologies (i.e., on-chain protocols), and to provide a safe harbor for assets that are the subject of an exploit. The text of the Agreement is [located/hosted/pinned at \_\_\_\_\_\_]. This Proposal's aim is to provide an on-chain indication of our Protocol Community's agreement to the Agreement as of the date of successful passing and execution.
13 |
14 | \item \textbf{Specification} - Technical and (if applicable) legal specifications around the Proposal's intended effects and actions. Specify target method(s) and argument(s), and all necessary off-chain signaled effects, actions, key actors, and beneficiaries.
15 |
16 | \begin{enumerate}[label=\alph*.]
17 | \item Exercise care in entering the target contract address, target method signature, and target method arguments/parameters; if applicable, consult the Protocol documentation.
18 | \end{enumerate}
19 |
20 | \textit{Ex.} A successfully passed proposal will result in the Protocol and Protocol Community's revocable adoption of the Safe Harbor Agreement for Whitehats. The target is as follows: [Insert applicable function signature and params]
21 |
22 | \item \textbf{Benefits} - Describe the reasonable, intended benefits to the Protocol and Protocol Community of the proposal's implementation in quantitative and qualitative terms.
23 |
24 | \textit{Ex.} By adopting this Agreement, our protocol community would encourage Whitehats (as defined in the Agreement) to, pursuant to criteria set out in the Agreement, responsibly test, seek to penetrate, and otherwise exploit software which utilizes, incorporates, or is otherwise complementary to our protocol, and potentially receive a reward for conducting such exploits. Following our protocol community's adoption, only those Whitehats who agree to the terms of the Agreement and act accordingly would therefore be eligible for rewards; this way, the specific parameters of Eligible Funds Rescue and reward procedures are agreed in advance, so frenzied rescues and negotiations immediately after exploits can be substantially mitigated. Adoption of the Agreement could generally provide a strong complement to protocol audits for ongoing security.
25 |
26 | \item \textbf{Detriments} - Disclose reasonably foreseeable harm, damages, risks, and liabilities to the Protocol and Protocol Community resulting from this proposal's implementation in quantitative and qualitative terms.
27 |
28 | \textit{Ex.} While the Safe Harbor Agreement for Whitehats has been drafted and reviewed by numerous developers and lawyers, it may have unintentional or unanticipated legal consequences, loopholes, or other deficiencies for the Protocol, Protocol Community, or Whitehats. The length and relative complexity of the Safe Harbor Agreement for Whitehats may deter otherwise willing Whitehats from engaging in activity that would be beneficial to the Protocol.
29 |
30 | \item \textbf{Summary of Options} - Clearly and succinctly summarize the vote options on this Proposal, especially if the options are more inclusive than simply \textit{For} or \textit{Against}.
31 |
32 | \textit{Ex.} For: Adopt the Safe Harbor Agreement for Whitehats. Against: Take no action.
33 |
34 | \item \textbf{Summary of Proposed DAO Adoption Procedures} - State the proposed parameterization of the adoptSafeHarbor function call for the DAO Adoption Procedures.
35 | \end{enumerate}
36 |
--------------------------------------------------------------------------------
/js/main.js:
--------------------------------------------------------------------------------
1 | import Web3 from "web3";
2 | import sigUtil from 'eth-sig-util';
3 | import fs from 'fs';
4 |
5 | // Your contract's ABI and address
6 | const artifactPath = '../registry-contracts/out/SafeHarborRegistry.sol/SafeHarborRegistry.json';
7 | const contractAddress = "0x8f72fcf695523A6FC7DD97EafDd7A083c386b7b6";
8 |
9 | const web3 = new Web3("http://localhost:8545");
10 | const accounts = await web3.eth.getAccounts();
11 | const signer = accounts[0];
12 | const signerPrivateKey = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
13 |
14 | // Example AgreementDetailsV1 data to be signed
15 | const domain = {
16 | name: "Safe Harbor",
17 | version: "1.0.0",
18 | chainId: 31337,
19 | verifyingContract: contractAddress
20 | };
21 |
22 | const types = {
23 | EIP712Domain: [
24 | { name: "name", type: "string" },
25 | { name: "version", type: "string" },
26 | { name: "chainId", type: "uint256" },
27 | { name: "verifyingContract", type: "address" },
28 | ],
29 | AgreementDetailsV1: [
30 | { name: "protocolName", type: "string" },
31 | { name: "contactDetails", type: "Contact[]" },
32 | { name: "chains", type: "Chain[]" },
33 | { name: "bountyTerms", type: "BountyTerms" },
34 | { name: "agreementURI", type: "string" }
35 | ],
36 | Contact: [
37 | { name: "name", type: "string" },
38 | { name: "contact", type: "string" }
39 | ],
40 | Chain: [
41 | { name: "assetRecoveryAddress", type: "address" },
42 | { name: "accounts", type: "Account[]" },
43 | { name: "id", type: "uint256" }
44 | ],
45 | Account: [
46 | { name: "accountAddress", type: "address" },
47 | { name: "childContractScope", type: "uint8" },
48 | { name: "signature", type: "bytes" }
49 | ],
50 | BountyTerms: [
51 | { name: "bountyPercentage", type: "uint256" },
52 | { name: "bountyCapUSD", type: "uint256" },
53 | { name: "retainable", type: "bool" },
54 | { name: "identity", type: "uint8" },
55 | { name: "diligenceRequirements", type: "string" }
56 | ]
57 | };
58 |
59 | // Replace this with actual data
60 | const value = {
61 | protocolName: "ExampleProtocol",
62 | contactDetails: [
63 | {
64 | name: "ExampleContact",
65 | contact: "contact@example.com"
66 | }
67 | ],
68 | chains: [
69 | {
70 | assetRecoveryAddress: "0xa83114a443da1cecefc50368531cace9f37fcccb",
71 | accounts: [
72 | {
73 | accountAddress: signer,
74 | childContractScope: 0,
75 | signature: "0x"
76 | }
77 | ],
78 | id: 1
79 | }
80 | ],
81 | bountyTerms: {
82 | bountyPercentage: 10,
83 | bountyCapUSD: 1000,
84 | retainable: true,
85 | identity: 0,
86 | diligenceRequirements: "Some requirements"
87 | },
88 | agreementURI: "https://example.com/agreement"
89 | };
90 |
91 | async function loadABI(path) {
92 | try {
93 | const artifact = JSON.parse(fs.readFileSync(path, 'utf8'));
94 | const contractABI = artifact.abi;
95 | return contractABI;
96 | } catch (error) {
97 | console.error('Error reading ABI:', error);
98 | return null;
99 | }
100 | }
101 |
102 | const primaryType = "AgreementDetailsV1";
103 |
104 | async function main() {
105 | // Create contract instance
106 | const abi = await loadABI(artifactPath);
107 | const contract = new web3.eth.Contract(abi, contractAddress);
108 |
109 | // Sign the typed data
110 | console.log("Signer:", signer);
111 |
112 | const typedData = {
113 | types,
114 | domain,
115 | primaryType,
116 | message: value
117 | };
118 |
119 | const signature = sigUtil.signTypedData_v4(Buffer.from(signerPrivateKey.slice(2), 'hex'), { data: typedData });
120 |
121 | // Recover the address from the typed data and signature
122 | const recoveredAddress = sigUtil.recoverTypedSignature_v4({
123 | data: typedData,
124 | sig: signature,
125 | });
126 |
127 | if (recoveredAddress.toLowerCase() !== signer.toLowerCase()) {
128 | console.error("Invalid signature");
129 | console.error("Expected:", signer);
130 | console.error("Recovered:", recoveredAddress);
131 | }
132 |
133 | const account = {
134 | accountAddress: signer,
135 | childContractScope: 0,
136 | signature
137 | };
138 |
139 | const isValid = await contract.methods.validateAccount(value, account).call();
140 | console.log("isValid:", isValid);
141 | }
142 |
143 | const identityEnumMap = {
144 | 0: "Anonymous",
145 | 1: "Pseudonymous",
146 | 2: "Named",
147 | }
148 |
149 | const childScopeEnumMap = {
150 | 0: "None",
151 | 1: "ExistingOnly",
152 | 2: "All",
153 | }
154 |
155 | // apply the enum maps to the value object
156 | function prettifyValue(value) {
157 |
158 | }
159 |
160 | main().catch(console.error);
161 |
--------------------------------------------------------------------------------
/documents/exhibits/a.tex:
--------------------------------------------------------------------------------
1 | For purposes of this Agreement, the capitalized terms set forth on Exhibit A shall have the definitions that are ascribed to them below:
2 |
3 | \begin{enumerate}[label=\Alph*.]
4 | \item "\textbf{Affiliate}" means, with respect to any Person, another Person that directly or indirectly, through one or more intermediaries, controls, is controlled by, or is under common control with, such first Person.
5 |
6 | \item "\textbf{Assets}" means the crypto-assets transacted on or in connection with an Eligible Funds Rescue in relation to the Protocol.
7 |
8 | \item "\textbf{Claim}" means all past, present and future disputes, claims, controversies, demands, rights, obligations, liabilities, actions and causes of action of every kind and nature (whether matured or unmatured, absolute or contingent, known or unknown, suspect or unsuspected, disclosed or undisclosed).
9 |
10 | \item "\textbf{Damages}" means any loss, damage, injury, decline in value, lost opportunity, Liability, claim, demand, settlement, judgment, award, fine, penalty, tax, fee (including reasonable attorneys' fees), charge, costs (including costs of investigation) or expense of any nature.
11 |
12 | \item "\textbf{Entity}" means any corporation (including any non-profit corporation), general partnership, limited partnership, limited liability partnership, joint venture, estate, trust, company (including any limited liability company or joint stock company), firm or other enterprise, association, organization or entity.
13 |
14 | \item "\textbf{Governmental Entity}" means any: (a) nation, multinational, supranational, state, commonwealth, province, territory, county, municipality, district or other jurisdiction of any nature; (b) federal, state, provincial, local, municipal, foreign or other government; (c) instrumentality, subdivision, department, ministry, board, court, administrative agency or commission or other governmental Entity, authority or instrumentality or political subdivision thereof; or (d) any quasi-governmental or private body exercising any executive, legislative, judicial, regulatory, taxing, importing or other governmental functions.
15 |
16 | \item "\textbf{Intellectual Property License}" means any license, sublicense, right, covenant, non-assertion, permission, immunity, consent, release or waiver under or with respect to any Intellectual Property Rights or Technology.
17 |
18 | \item "\textbf{Intellectual Property Rights}" means any and all rights in intellectual property and/or industrial property anywhere in the world, whether arising under statute, common law or otherwise.
19 |
20 | \item "\textbf{Legal Proceeding}" means any action, suit, litigation, arbitration, claim, proceeding (including any civil, criminal, administrative, investigative or appellate proceeding), hearing, inquiry, audit, examination or investigation commenced, brought, conducted or heard by or before, or otherwise involving, any court or other Governmental Entity or any arbitrator or arbitration panel.
21 |
22 | \item "\textbf{Legal Requirement}" means any: (a) federal, state, local, municipal, foreign, supranational or other law, statute, constitution, treaty, principle of common law, directive, resolution, ordinance, code, rule, regulation, judgment, ruling or requirement issued, enacted, adopted, promulgated, implemented or otherwise put into effect by or under the authority of any Governmental Entity; or (b) order, writ, injunction, judgment, edict, decree, ruling or award of any arbitrator or any court or other Governmental Entity.
23 |
24 | \item "\textbf{Liability}" means any debt, obligation, duty or liability of any nature (including any unknown, undisclosed, unmatured, unaccrued, unasserted, contingent, indirect, conditional, implied, vicarious, derivative, joint, several or secondary liability), regardless of whether such debt, obligation, duty or liability is immediately due and payable.
25 |
26 | \item "\textbf{Parties}" means the Protocol Community, Protocol Community Members and Whitehats participating in the Program, to whom these Terms apply.
27 |
28 | \item "\textbf{Person}" means any individual, Entity or Governmental Entity.
29 |
30 | \item "\textbf{Program}" means the process set out in this Agreement to incentivize Eligible Funds Rescues whereby a Whitehat may seek to conduct an Exploit and transfer Tokens to the Asset Recovery Address as further detailed in this Agreement.
31 |
32 | \item "\textbf{Representatives}" of a Person means such Person's officers, directors, employees, agents, attorneys, accountants, advisors and representatives.
33 |
34 | \item "\textbf{SEAL}" means the Open Security Alliance Inc., a Texas corporation.
35 |
36 | \item "\textbf{Technology}" means any and all: (a) technology, formulae, algorithms, procedures, processes, methods, techniques, ideas, know-how, creations, inventions, discoveries, and improvements (whether patentable or unpatentable and whether or not reduced to practice); (b) technical, engineering, manufacturing, product, marketing, servicing, business, financial, supplier, personnel and other information and materials; (c) specifications, designs, industrial designs, models, devices, prototypes, schematics and development tools; (d) software, websites, content, images, logos, graphics, text, photographs, artwork, audiovisual works, sound recordings, graphs, drawings, reports, analyses, writings, and other works of authorship and copyrightable subject matter; and (e) databases and other compilations and collections of data or information.
37 | \end{enumerate}
38 |
--------------------------------------------------------------------------------
/registry-contracts/script/v1/AdoptSafeHarborV1.s.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: UNLICENSED
2 | pragma solidity ^0.8.13;
3 |
4 | import {console} from "forge-std/console.sol";
5 | import {stdJson} from "forge-std/StdJson.sol";
6 | import {ScriptBase} from "forge-std/Base.sol";
7 | import "../../src/v1/SafeHarborRegistry.sol";
8 |
9 | contract AdoptSafeHarborV1 is ScriptBase {
10 | using stdJson for string;
11 |
12 | function run() public {
13 | uint256 deployerPrivateKey = vm.envUint("DEPLOYER_PRIVATE_KEY");
14 | SafeHarborRegistry registry = SafeHarborRegistry(0x8f72fcf695523A6FC7DD97EafDd7A083c386b7b6);
15 | string memory json = vm.readFile("agreementDetails.json");
16 |
17 | adopt(deployerPrivateKey, registry, json);
18 | }
19 |
20 | function adopt(uint256 deployerPrivateKey, SafeHarborRegistry registry, string memory json) public {
21 | // Read and parse the JSON file
22 | bytes memory data = json.parseRaw(".");
23 |
24 | // Decode into the intermediary struct
25 | agreementDetailsV1JSON memory jsonDetails = abi.decode(data, (agreementDetailsV1JSON));
26 | AgreementDetailsV1 memory details = mapAgreementDetails(jsonDetails);
27 |
28 | // Begin broadcast
29 | vm.startBroadcast(deployerPrivateKey);
30 |
31 | registry.adoptSafeHarbor(details);
32 |
33 | // End broadcast
34 | vm.stopBroadcast();
35 | }
36 |
37 | // Helper function to map agreementDetailsV1JSON to AgreementDetailsV1
38 | function mapAgreementDetails(agreementDetailsV1JSON memory jsonDetails)
39 | internal
40 | pure
41 | returns (AgreementDetailsV1 memory)
42 | {
43 | return AgreementDetailsV1({
44 | protocolName: jsonDetails.protocolName,
45 | contactDetails: mapContacts(jsonDetails.contact),
46 | chains: mapChains(jsonDetails.chains),
47 | bountyTerms: mapBountyTerms(jsonDetails.bountyTerms),
48 | agreementURI: jsonDetails.agreementURI
49 | });
50 | }
51 |
52 | // Define intermediary structs with fields in alphabetical order
53 | // and enums replaced with uint8 for decoding
54 | struct agreementDetailsV1JSON {
55 | string agreementURI;
56 | bountyTermsJSON bountyTerms;
57 | chainJSON[] chains;
58 | contactJSON[] contact;
59 | string protocolName;
60 | }
61 |
62 | struct bountyTermsJSON {
63 | uint256 bountyCapUSD;
64 | uint256 bountyPercentage;
65 | string diligenceRequirements;
66 | uint8 identity; // enum replaced with uint8
67 | bool retainable;
68 | }
69 |
70 | struct chainJSON {
71 | accountJSON[] accounts;
72 | address assetRecoveryAddress;
73 | uint256 id;
74 | }
75 |
76 | struct accountJSON {
77 | address accountAddress;
78 | uint8 childContractScope; // enum replaced with uint8
79 | bytes signature;
80 | }
81 |
82 | struct contactJSON {
83 | string contact;
84 | string name;
85 | }
86 |
87 | // Helper function to map contactJSON[] to Contact[]
88 | function mapContacts(contactJSON[] memory jsonContacts) internal pure returns (Contact[] memory) {
89 | uint256 count = jsonContacts.length;
90 | Contact[] memory contacts = new Contact[](count);
91 | for (uint256 i = 0; i < count; i++) {
92 | contacts[i] = Contact({contact: jsonContacts[i].contact, name: jsonContacts[i].name});
93 | }
94 | return contacts;
95 | }
96 |
97 | // Helper function to map bountyTermsJSON to BountyTerms
98 | function mapBountyTerms(bountyTermsJSON memory jsonBountyTerms) internal pure returns (BountyTerms memory) {
99 | return BountyTerms({
100 | bountyPercentage: jsonBountyTerms.bountyPercentage,
101 | bountyCapUSD: jsonBountyTerms.bountyCapUSD,
102 | retainable: jsonBountyTerms.retainable,
103 | identity: IdentityRequirements(jsonBountyTerms.identity),
104 | diligenceRequirements: jsonBountyTerms.diligenceRequirements
105 | });
106 | }
107 |
108 | // Helper function to map chainJSON[] to Chain[]
109 | function mapChains(chainJSON[] memory jsonChains) internal pure returns (Chain[] memory) {
110 | uint256 count = jsonChains.length;
111 | Chain[] memory chains = new Chain[](count);
112 | for (uint256 i = 0; i < count; i++) {
113 | chains[i] = Chain({
114 | accounts: mapAccounts(jsonChains[i].accounts),
115 | assetRecoveryAddress: jsonChains[i].assetRecoveryAddress,
116 | id: jsonChains[i].id
117 | });
118 | }
119 | return chains;
120 | }
121 |
122 | // Helper function to map accountJSON[] to Account[]
123 | function mapAccounts(accountJSON[] memory jsonAccounts) internal pure returns (Account[] memory) {
124 | uint256 count = jsonAccounts.length;
125 | Account[] memory accounts = new Account[](count);
126 | for (uint256 i = 0; i < count; i++) {
127 | accounts[i] = Account({
128 | accountAddress: jsonAccounts[i].accountAddress,
129 | childContractScope: ChildContractScope(jsonAccounts[i].childContractScope),
130 | signature: jsonAccounts[i].signature
131 | });
132 | }
133 | return accounts;
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/registry-contracts/test/v2/SafeHarborRegistryV2.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: UNLICENSED
2 | pragma solidity ^0.8.20;
3 |
4 | /// @notice Imporing these packages directly due to naming conflicts between "Account" and "Chain" structs.
5 | import {TestBase} from "forge-std/Test.sol";
6 | import {DSTest} from "ds-test/test.sol";
7 | import {console} from "forge-std/console.sol";
8 | import {Vm} from "forge-std/Vm.sol";
9 | import {SafeHarborRegistryV2, IRegistry} from "../../src/v2/SafeHarborRegistryV2.sol";
10 | import {AgreementV2} from "../../src/v2/AgreementV2.sol";
11 | import "../../src/v2/AgreementDetailsV2.sol";
12 | import "../../script/v2/AdoptSafeHarborV2.s.sol";
13 | import "./mock.sol";
14 |
15 | contract SafeHarborRegistryV2Test is TestBase, DSTest {
16 | address owner;
17 |
18 | SafeHarborRegistryV2 registry;
19 | AgreementDetailsV2 details;
20 | address agreementAddress;
21 |
22 | function setUp() public {
23 | owner = address(0x1);
24 |
25 | registry = new SafeHarborRegistryV2(owner);
26 |
27 | {
28 | string[] memory validChains = new string[](2);
29 | validChains[0] = "eip155:1";
30 | validChains[1] = "eip155:2";
31 | vm.prank(owner);
32 | registry.setValidChains(validChains);
33 | }
34 |
35 | //? NO clue why, but apparently this is needed to avoid a stack too deep
36 | //? error?
37 | AgreementFactoryV2 factory = new AgreementFactoryV2();
38 | details = getMockAgreementDetails("0xaabbccdd");
39 | agreementAddress = factory.create(details, address(registry), owner);
40 | }
41 |
42 | function test_setValidChains() public {
43 | string[] memory caip2ChainIds = new string[](2);
44 | caip2ChainIds[0] = "eip155:1";
45 | caip2ChainIds[1] = "eip155:137";
46 |
47 | // Should fail if not called by owner
48 | vm.expectRevert();
49 | registry.setValidChains(caip2ChainIds);
50 |
51 | // Should succeed if called by owner
52 | vm.expectEmit();
53 | emit SafeHarborRegistryV2.ChainValiditySet(caip2ChainIds[0], true);
54 | vm.expectEmit();
55 | emit SafeHarborRegistryV2.ChainValiditySet(caip2ChainIds[1], true);
56 | vm.prank(owner);
57 | registry.setValidChains(caip2ChainIds);
58 |
59 | // Verify chains are valid
60 | assertTrue(registry.isChainValid("eip155:1"));
61 | assertTrue(registry.isChainValid("eip155:2"));
62 | assertTrue(registry.isChainValid("eip155:137"));
63 | assertTrue(!registry.isChainValid("eip155:999"));
64 |
65 | string[] memory validChains = registry.getValidChains();
66 | assertEq(validChains[0], "eip155:1");
67 | assertEq(validChains[1], "eip155:2");
68 | assertEq(validChains[2], "eip155:137");
69 | assertEq(validChains.length, 3);
70 | }
71 |
72 | function test_setInvalidChains() public {
73 | string[] memory invalidChains = new string[](2);
74 | invalidChains[0] = "eip155:137";
75 | invalidChains[1] = "eip155:2";
76 |
77 | // Should fail if not called by owner
78 | vm.expectRevert();
79 | registry.setInvalidChains(invalidChains);
80 |
81 | // Should succeed if called by owner
82 | vm.expectEmit();
83 | emit SafeHarborRegistryV2.ChainValiditySet("eip155:137", false);
84 | vm.expectEmit();
85 | emit SafeHarborRegistryV2.ChainValiditySet("eip155:2", false);
86 | vm.prank(owner);
87 | registry.setInvalidChains(invalidChains);
88 |
89 | assertTrue(registry.isChainValid("eip155:1"));
90 | assertTrue(!registry.isChainValid("eip155:137"));
91 | assertTrue(!registry.isChainValid("eip155:2"));
92 |
93 | string[] memory remainingChains = registry.getValidChains();
94 | assertEq(remainingChains.length, 1);
95 | assertEq(remainingChains[0], "eip155:1");
96 | }
97 |
98 | function test_adoptSafeHarbor() public {
99 | address entity = address(0xee);
100 |
101 | vm.expectEmit();
102 | emit SafeHarborRegistryV2.SafeHarborAdoption(entity, address(0), agreementAddress);
103 | vm.prank(entity);
104 | registry.adoptSafeHarbor(agreementAddress);
105 | }
106 |
107 | function test_getDetails() public {
108 | address entity = address(0xee);
109 |
110 | vm.prank(entity);
111 | registry.adoptSafeHarbor(agreementAddress);
112 | address _agreement = registry.getAgreement(entity);
113 | assertEq(agreementAddress, _agreement);
114 | }
115 |
116 | function test_getDetails_fallback() public {
117 | address entity = address(0xee);
118 |
119 | vm.prank(owner);
120 | SafeHarborRegistryV2 newRegistry = new SafeHarborRegistryV2(owner);
121 | vm.prank(owner);
122 | newRegistry.setFallbackRegistry(IRegistry(address(registry)));
123 |
124 | vm.prank(entity);
125 | registry.adoptSafeHarbor(agreementAddress);
126 | address _agreement = newRegistry.getAgreement(entity);
127 | assertEq(agreementAddress, _agreement);
128 | }
129 |
130 | function test_getDetails_missing() public {
131 | address entity = address(0xee);
132 |
133 | vm.expectRevert(SafeHarborRegistryV2.NoAgreement.selector);
134 | address _agreement = registry.getAgreement(entity);
135 | assertEq(_agreement, address(0));
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/registry-contracts/src/v2/SafeHarborRegistryV2.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity ^0.8.20;
3 |
4 | import "./AgreementV2.sol" as V2;
5 | import "../common/IRegistry.sol";
6 | import "openzeppelin-contracts/contracts/access/Ownable.sol";
7 |
8 | string constant VERSION = "1.1.0";
9 |
10 | /// @title The Safe Harbor Registry. See www.securityalliance.org for details.
11 | contract SafeHarborRegistryV2 is Ownable {
12 | // ----- STATE VARIABLES -----
13 |
14 | /// @notice A mapping which records the agreement details for a given governance/admin address.
15 | mapping(address entity => address details) private agreements;
16 |
17 | /// @notice A mapping CAIP-2 IDs and if they are valid.
18 | mapping(string => bool) private validChains;
19 |
20 | /// @notice Array to keep track of all valid chain IDs
21 | string[] private validChainsList;
22 |
23 | /// @notice The fallback registry.
24 | IRegistry fallbackRegistry;
25 |
26 | // ----- EVENTS -----
27 |
28 | /// @notice An event that records when an address either newly adopts the Safe Harbor, or alters its previous terms.
29 | event SafeHarborAdoption(address indexed entity, address oldDetails, address newDetails);
30 |
31 | /// @notice An event that records when a chain is set as valid or invalid.
32 | event ChainValiditySet(string caip2ChainId, bool valid);
33 |
34 | // ----- ERRORS -----
35 |
36 | error NoAgreement();
37 |
38 | // ----- CONSTRUCTOR -----
39 |
40 | /// @notice Sets the factory and fallback registry addresses
41 | constructor(address _owner) Ownable(_owner) {}
42 |
43 | // ----- EXTERNAL FUNCTIONS -----
44 | function setFallbackRegistry(IRegistry _fallbackRegistry) external onlyOwner {
45 | fallbackRegistry = _fallbackRegistry;
46 | }
47 |
48 | function version() external pure returns (string memory) {
49 | return VERSION;
50 | }
51 |
52 | /// @notice Function that sets a list of chains as valid in the registry.
53 | /// @param _caip2ChainIds The CAIP-2 IDs of the chains to mark as valid.
54 | function setValidChains(string[] calldata _caip2ChainIds) external onlyOwner {
55 | for (uint256 i = 0; i < _caip2ChainIds.length; i++) {
56 | if (!validChains[_caip2ChainIds[i]]) {
57 | validChains[_caip2ChainIds[i]] = true;
58 | validChainsList.push(_caip2ChainIds[i]);
59 | }
60 | emit ChainValiditySet(_caip2ChainIds[i], true);
61 | }
62 | }
63 |
64 | /// @notice Function that marks a list of chains as invalid in the registry.
65 | /// @param _caip2ChainIds The CAIP-2 IDs of the chains to mark as invalid.
66 | function setInvalidChains(string[] calldata _caip2ChainIds) external onlyOwner {
67 | for (uint256 i = 0; i < _caip2ChainIds.length; i++) {
68 | if (validChains[_caip2ChainIds[i]]) {
69 | validChains[_caip2ChainIds[i]] = false;
70 | _removeFromValidChainsList(_caip2ChainIds[i]);
71 | }
72 | emit ChainValiditySet(_caip2ChainIds[i], false);
73 | }
74 | }
75 |
76 | /// @notice Function that creates a new AgreementV2 contract and records it as an adoption by msg.sender.
77 | /// @param agreementAddress The address of the agreement to adopt.
78 | function adoptSafeHarbor(address agreementAddress) external {
79 | address adopter = msg.sender;
80 |
81 | address oldDetails = agreements[adopter];
82 | agreements[adopter] = agreementAddress;
83 | emit SafeHarborAdoption(adopter, oldDetails, agreementAddress);
84 | }
85 |
86 | /// @notice Get the agreement address for the adopter. Recursively queries fallback registries.
87 | /// @param adopter The adopter to query.
88 | /// @return address The agreement address.
89 | function getAgreement(address adopter) external view returns (address) {
90 | address agreement = agreements[adopter];
91 |
92 | if (agreement != address(0)) {
93 | return agreement;
94 | }
95 |
96 | if (address(fallbackRegistry) != address(0)) {
97 | return fallbackRegistry.getAgreement(adopter);
98 | }
99 |
100 | revert NoAgreement();
101 | }
102 |
103 | /// @notice Function that returns if a chain is valid.
104 | /// @param _caip2ChainId The CAIP-2 ID of the chain to check.
105 | /// @return bool True if the chain is valid, false otherwise.
106 | function isChainValid(string calldata _caip2ChainId) external view returns (bool) {
107 | return validChains[_caip2ChainId];
108 | }
109 |
110 | /// @notice Function that returns all currently valid chain IDs.
111 | /// @return string[] Array of all valid CAIP-2 chain IDs.
112 | function getValidChains() external view returns (string[] memory) {
113 | return validChainsList;
114 | }
115 |
116 | // ----- INTERNAL FUNCTIONS -----
117 |
118 | /// @notice Internal function to remove a chain ID from the valid chains list.
119 | /// @param _caip2ChainId The CAIP-2 chain ID to remove.
120 | function _removeFromValidChainsList(string calldata _caip2ChainId) internal {
121 | for (uint256 i = 0; i < validChainsList.length; i++) {
122 | if (keccak256(bytes(validChainsList[i])) == keccak256(bytes(_caip2ChainId))) {
123 | // Replace with last element and pop
124 | validChainsList[i] = validChainsList[validChainsList.length - 1];
125 | validChainsList.pop();
126 | break;
127 | }
128 | }
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/assets/Security-Alliance-Logo-Blue.svg:
--------------------------------------------------------------------------------
1 |
31 |
--------------------------------------------------------------------------------
/documents/exhibits/e.tex:
--------------------------------------------------------------------------------
1 | Participation in the Security Alliance Whitehat Safe Harbor Agreement (the "Program") carries a high degree of consequential risk. You should carefully consider the risks described below together with information presented in the Summary of the Program available at [\_\_\_\_\_\_\_\_\_\_] and the version of the Program Agreement maintained by the protocol which you wish to engage in advance of engaging it under the terms of the Agreement. Also consulting tax and legal advisors in advance of participation is also strongly recommended.
2 |
3 | \subsubsection*{Terms capitalized below are defined in the template Protocol Agreement found at [\_\_\_\_\_\_\_\_\_\_].}
4 |
5 | \subsubsection*{The Protocol Community must properly implement the Program for the Protocol you are targeting before you engage the Program}\label{exhibit:e:implementation}
6 |
7 | You should confirm that the Protocol Community has properly implemented the Program, including by reviewing the applicable Adoption Form, before participating in the Program. If the Program is not properly implemented, it is likely that some or all of the terms of the Program Agreement will be unenforceable, which could expose you to liability from claims from certain Protocol Community Members or other Users.
8 |
9 | \subsubsection*{You are expected to have experience with and expert-level knowledge of blockchain systems and cybersecurity as a condition of your participation}\label{exhibit:e:expertise}
10 |
11 | Given the nature of the activities that you will perform by participating in the Program, you should be highly skilled as a cybersecurity professional and believe that you will likely be able to succeed in your attempted rescue of the Protocol. If you cannot make these commitments, you should seriously consider the potential risks before engaging in the Program, including the risk that you might inadvertently violate relevant laws or regulations by seeking to undertake an Eligible Funds Rescue.
12 |
13 | \subsubsection*{Failing to notify the Protocol Community that you are attempting a rescue may block your ability to obtain the Reward}\label{exhibit:e:notification}
14 |
15 | As provided for in the Program Agreement, you should notify the Protocol Community that you are engaging in the Program, proper ways to contact the Protocol Community will be provided in the Program Agreement.
16 |
17 | \subsubsection*{Failure to successfully send all Returnable Assets to the Asset Recovery Address may prevent you from getting a Bounty}\label{exhibit:e:asset_transfer}
18 |
19 | Certain Protocol Communities may require you to deposit all Returnable Assets to the Asset Recovery Address, and failure to do so could constitute a violation of the terms of the Agreement. Moreover, even if you put a lot of work into trying to save assets or succeed in recovering \textit{most} of them, there is no guarantee that you will receive any sort of reward or compensation for your effort and time.
20 |
21 | \subsubsection*{Legal proceedings ongoing, pending, or threatened against you may make you ineligible for the Program}\label{exhibit:e:legal_proceedings}
22 |
23 | If you are involved in any legal proceedings or think you may be before completing your obligations and receiving your Reward under the Program Agreement, you should not engage the Program.
24 |
25 | \subsubsection*{You agree to follow certain procedures in case you become eligible for a Bounty and it is not delivered in a timely manner or the amount of the Bounty is disputed}\label{exhibit:e:dispute_procedures}
26 |
27 | The Protocol Agreement details resolution steps to be taken if there is disagreement over the Bounty amount; however, you will not be able to sue any other party to the Program Agreement because of this disagreement.
28 |
29 | \subsubsection*{You will be responsible for any tax liability incurred as a result of receiving the Bounty}\label{exhibit:e:tax_liability}
30 |
31 | The Protocol Community will not assist you in filing or structuring the Bounty for tax treatment in a way not described in the Program Agreement. You should be familiar with your tax obligations in your local jurisdiction before engaging the Program.
32 |
33 | \subsubsection*{This Program cannot protect you from incurring criminal, regulatory, or other liability as a result of your participation}\label{exhibit:e:liability_limits}
34 |
35 | Although the Program may shield you from certain claims brought by the Protocol Community and its Members, no contract is able to prevent or preempt criminal, regulatory, or other liability. Moreover, legal claims may still be brought against you by third parties, who are not subject to this Agreement and its release provisions.
36 |
37 | \subsubsection*{No partnership or endorsement is formed among you and any member of the Protocol Community}\label{exhibit:e:no_partnership}
38 |
39 | The Protocol Community is not engaging you as a partner, agent, or contractor. No relationship beyond that arising from being a party to the Program Agreement is formed through participation in the Program.
40 |
41 | \subsubsection*{Indemnity of Protocol Community, Members, Affiliates}\label{exhibit:e:indemnity}
42 |
43 | In cases where members of the Protocol Community or their Affiliates incur liability to others as a result of your actions under the Program, you will indemnify (reimburse their expenses) those parties.
44 |
45 | \subsubsection*{You agree to follow certain procedures in case there is a dispute about the Agreement}\label{exhibit:e:dispute_agreement}
46 |
47 | In case there is a dispute about the Program Agreement, you will not be able to sue any party to the Agreement. Instead, your dispute must be arbitrated in the jurisdiction of the Singapore International Commercial Court using the rules of the Singapore International Arbitration Centre.
48 |
49 | \subsubsection*{The Agreement may not be enforceable in all jurisdictions or against all relevant persons}\label{exhibit:e:enforceability}
50 |
51 | The default jurisdiction of the Program is under the Singapore International Commercial Court. Although the Agreement is a legally binding contract, it cannot be guaranteed to be enforceable in all jurisdictions or under all circumstances. Some people in the Protocol Community and some third parties may still have claims against you that cannot be released through this Agreement.
52 |
53 | \subsubsection*{You understand that if you profit in any way other than the Bounty, you may incur significant risk of liability}\label{exhibit:e:profit_risks}
54 |
55 | Profiting in other ways from conducting this Exploit may constitute securities or commodities manipulation or fraud and has been prosecuted in that fashion in the past. Engaging in fraudulent or manipulative conduct is not covered by the release of liability under this program.
56 |
--------------------------------------------------------------------------------
/registry-contracts/script/v2/AdoptSafeHarborV2.s.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: UNLICENSED
2 | pragma solidity ^0.8.13;
3 |
4 | import {console} from "forge-std/console.sol";
5 | import {stdJson} from "forge-std/StdJson.sol";
6 | import {ScriptBase} from "forge-std/Base.sol";
7 | import "../../src/v2/SafeHarborRegistryV2.sol";
8 | import "../../src/v2/AgreementFactoryV2.sol";
9 | import {
10 | AgreementDetailsV2,
11 | Chain as ChainV2,
12 | Account as AccountV2,
13 | Contact,
14 | BountyTerms,
15 | ChildContractScope,
16 | IdentityRequirements
17 | } from "../../src/v2/AgreementDetailsV2.sol";
18 | import "./DeployRegistryV2.s.sol";
19 | import {logAgreementDetails} from "../../test/v2/mock.sol";
20 |
21 | contract AdoptSafeHarborV2 is ScriptBase {
22 | using stdJson for string;
23 |
24 | // Update these addresses to match your deployed contracts
25 | address constant REGISTRY_ADDRESS = 0x1eaCD100B0546E433fbf4d773109cAD482c34686;
26 | address constant FACTORY_ADDRESS = 0x98D1594Ba4f2115f75392ac92A7e3C8A81C67Fed;
27 |
28 | function run() public {
29 | uint256 deployerPrivateKey = vm.envUint("DEPLOYER_PRIVATE_KEY");
30 | address owner = vm.envOr("AGREEMENT_OWNER", vm.addr(deployerPrivateKey));
31 | bool shouldAdoptToRegistry = vm.envOr("ADOPT_TO_REGISTRY", false);
32 |
33 | SafeHarborRegistryV2 registry = SafeHarborRegistryV2(REGISTRY_ADDRESS);
34 | AgreementFactoryV2 factory = AgreementFactoryV2(FACTORY_ADDRESS);
35 |
36 | string memory json = vm.readFile("agreementDetailsV2.json");
37 |
38 | adopt(deployerPrivateKey, registry, factory, json, owner, shouldAdoptToRegistry);
39 | }
40 |
41 | function adopt(
42 | uint256 deployerPrivateKey,
43 | SafeHarborRegistryV2 registry,
44 | AgreementFactoryV2 factory,
45 | string memory json,
46 | address owner,
47 | bool shouldAdoptToRegistry
48 | ) public {
49 | AgreementDetailsV2 memory details = parseAgreementDetails(json);
50 | logAgreementDetails(details);
51 |
52 | vm.startBroadcast(deployerPrivateKey);
53 |
54 | address agreementAddress = factory.create(details, address(registry), owner);
55 | console.log("Created agreement at:");
56 | console.logAddress(agreementAddress);
57 |
58 | if (shouldAdoptToRegistry) {
59 | registry.adoptSafeHarbor(agreementAddress);
60 | }
61 |
62 | vm.stopBroadcast();
63 | }
64 |
65 | // Helper function to parse the complete agreement details
66 | function parseAgreementDetails(string memory json) public view returns (AgreementDetailsV2 memory) {
67 | return AgreementDetailsV2({
68 | protocolName: json.readString(".protocolName"),
69 | contactDetails: parseContacts(json),
70 | chains: parseChains(json),
71 | bountyTerms: parseBountyTerms(json),
72 | agreementURI: json.readString(".agreementURI")
73 | });
74 | }
75 |
76 | // Helper function to parse contacts array
77 | function parseContacts(string memory json) public view returns (Contact[] memory) {
78 | uint256 contactCount = getArrayLength(json, ".contact", ".name");
79 |
80 | Contact[] memory contacts = new Contact[](contactCount);
81 | for (uint256 i = 0; i < contactCount; i++) {
82 | contacts[i] = Contact({
83 | name: json.readString(string.concat(".contact[", vm.toString(i), "].name")),
84 | contact: json.readString(string.concat(".contact[", vm.toString(i), "].contact"))
85 | });
86 | }
87 | return contacts;
88 | }
89 |
90 | // Helper function to parse bounty terms
91 | function parseBountyTerms(string memory json) public pure returns (BountyTerms memory) {
92 | return BountyTerms({
93 | bountyPercentage: json.readUint(".bountyTerms.bountyPercentage"),
94 | bountyCapUSD: json.readUint(".bountyTerms.bountyCapUSD"),
95 | retainable: json.readBool(".bountyTerms.retainable"),
96 | identity: IdentityRequirements(uint8(json.readUint(".bountyTerms.identity"))),
97 | diligenceRequirements: json.readString(".bountyTerms.diligenceRequirements"),
98 | aggregateBountyCapUSD: json.readUint(".bountyTerms.aggregateBountyCapUSD")
99 | });
100 | }
101 |
102 | // Helper function to parse chains array
103 | function parseChains(string memory json) public view returns (ChainV2[] memory) {
104 | uint256 chainCount = getArrayLength(json, ".chains", ".id");
105 |
106 | ChainV2[] memory chains = new ChainV2[](chainCount);
107 | for (uint256 i = 0; i < chainCount; i++) {
108 | string memory chainIndex = vm.toString(i);
109 | chains[i] = ChainV2({
110 | caip2ChainId: json.readString(string.concat(".chains[", chainIndex, "].id")),
111 | assetRecoveryAddress: json.readString(string.concat(".chains[", chainIndex, "].assetRecoveryAddress")),
112 | accounts: parseAccounts(json, chainIndex)
113 | });
114 | }
115 | return chains;
116 | }
117 |
118 | // Helper function to parse accounts for a specific chain
119 | function parseAccounts(string memory json, string memory chainIndex) public view returns (AccountV2[] memory) {
120 | uint256 accountCount =
121 | getArrayLength(json, string.concat(".chains[", chainIndex, "].accounts"), ".accountAddress");
122 |
123 | AccountV2[] memory accounts = new AccountV2[](accountCount);
124 | for (uint256 j = 0; j < accountCount; j++) {
125 | string memory accountIndex = vm.toString(j);
126 | accounts[j] = AccountV2({
127 | accountAddress: json.readString(
128 | string.concat(".chains[", chainIndex, "].accounts[", accountIndex, "].accountAddress")
129 | ),
130 | childContractScope: ChildContractScope(
131 | uint8(
132 | json.readUint(
133 | string.concat(".chains[", chainIndex, "].accounts[", accountIndex, "].childContractScope")
134 | )
135 | )
136 | )
137 | });
138 | }
139 | return accounts;
140 | }
141 |
142 | // Helper function to determine array length by checking if indices exist
143 | function getArrayLength(string memory json, string memory arrayPath, string memory testField)
144 | internal
145 | view
146 | returns (uint256)
147 | {
148 | uint256 count = 0;
149 | while (true) {
150 | string memory fullPath = string.concat(arrayPath, "[", vm.toString(count), "]", testField);
151 | if (!vm.keyExists(json, fullPath)) {
152 | break;
153 | }
154 | count++;
155 | }
156 | return count;
157 | }
158 | }
159 |
--------------------------------------------------------------------------------
/registry-contracts/src/v1/AgreementValidatorV1.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity ^0.8.20;
3 |
4 | import "./SignatureValidator.sol";
5 | import "./AgreementV1.sol";
6 | import "./EIP712.sol";
7 |
8 | /// @notice Validator contract that validates safe harbor agreements.
9 | contract AgreementValidatorV1 is SignatureValidator, EIP712("Safe Harbor", _version) {
10 | /// ----- eip-712 TYPEHASHES
11 | /// GENERATED WITH `forge eip712 registry-contracts/src/AgreementV1.sol`
12 | bytes private constant agreementDetailsTypeHashStr =
13 | "AgreementDetailsV1(string protocolName,Contact[] contactDetails,Chain[] chains,BountyTerms bountyTerms,string agreementURI)Account(address accountAddress,uint8 childContractScope,bytes signature)BountyTerms(uint256 bountyPercentage,uint256 bountyCapUSD,bool retainable,uint8 identity,string diligenceRequirements)Chain(address assetRecoveryAddress,Account[] accounts,uint256 id)Contact(string name,string contact)";
14 | bytes private constant contactTypeHashStr = "Contact(string name,string contact)";
15 | bytes private constant chainTypeHashStr =
16 | "Chain(address assetRecoveryAddress,Account[] accounts,uint256 id)Account(address accountAddress,uint8 childContractScope,bytes signature)";
17 | bytes private constant accountTypeHashStr =
18 | "Account(address accountAddress,uint8 childContractScope,bytes signature)";
19 | bytes private constant bountyTermsTypeHashStr =
20 | "BountyTerms(uint256 bountyPercentage,uint256 bountyCapUSD,bool retainable,uint8 identity,string diligenceRequirements)";
21 |
22 | bytes32 private constant AGREEMENTDETAILS_TYPEHASH = keccak256(agreementDetailsTypeHashStr);
23 | bytes32 private constant CONTACT_TYPEHASH = keccak256(contactTypeHashStr);
24 | bytes32 private constant CHAIN_TYPEHASH = keccak256(chainTypeHashStr);
25 | bytes32 private constant ACCOUNT_TYPEHASH = keccak256(accountTypeHashStr);
26 | bytes32 private constant BOUNTYTERMS_TYPEHASH = keccak256(bountyTermsTypeHashStr);
27 |
28 | /// @notice Function that validates an account's signature for the agreement.
29 | /// @param details The details of the agreement.
30 | /// @param account The account to validate.
31 | function validateAccount(AgreementDetailsV1 memory details, Account memory account) public view returns (bool) {
32 | // Hash the details with eip-712.
33 | bytes32 digest = getTypedDataHash(details);
34 | return isSignatureValid(account.accountAddress, digest, account.signature);
35 | }
36 |
37 | /// @notice Function that validates an account's signature for the agreement using an agreement address.
38 | /// @param agreementAddress The address of the deployed AgreementV1 contract.
39 | /// @param account The account to validate.
40 | function validateAccountByAddress(address agreementAddress, Account memory account) external view returns (bool) {
41 | AgreementV1 agreement = AgreementV1(agreementAddress);
42 | AgreementDetailsV1 memory details = agreement.getDetails();
43 |
44 | return validateAccount(details, account);
45 | }
46 |
47 | /// ----- EIP-712 METHODS -----
48 | function getTypedDataHash(AgreementDetailsV1 memory details) public view returns (bytes32) {
49 | return keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR(), hash(details)));
50 | }
51 |
52 | function hash(AgreementDetailsV1 memory details) public pure returns (bytes32) {
53 | return keccak256(
54 | abi.encode(
55 | AGREEMENTDETAILS_TYPEHASH,
56 | hash(details.protocolName),
57 | hash(details.contactDetails),
58 | hash(details.chains),
59 | hash(details.bountyTerms),
60 | hash(details.agreementURI)
61 | )
62 | );
63 | }
64 |
65 | function hash(Contact[] memory contacts) internal pure returns (bytes32) {
66 | // Array values are encoded as the keccak256 of the concatenation of the encoded values.
67 | // https://eips.ethereum.org/EIPS/eip-712#definition-of-encodedata
68 | bytes memory encoded;
69 | for (uint256 i = 0; i < contacts.length; i++) {
70 | encoded = abi.encodePacked(encoded, hash(contacts[i]));
71 | }
72 |
73 | return keccak256(encoded);
74 | }
75 |
76 | function hash(Contact memory contact) internal pure returns (bytes32) {
77 | return keccak256(abi.encode(CONTACT_TYPEHASH, hash(contact.name), hash(contact.contact)));
78 | }
79 |
80 | function hash(Chain[] memory chains) internal pure returns (bytes32) {
81 | // Array values are encoded as the keccak256 of the concatenation of the encoded values.
82 | // https://eips.ethereum.org/EIPS/eip-712#definition-of-encodedata
83 | bytes memory encoded;
84 | for (uint256 i = 0; i < chains.length; i++) {
85 | encoded = abi.encodePacked(encoded, hash(chains[i]));
86 | }
87 |
88 | return keccak256(encoded);
89 | }
90 |
91 | function hash(Chain memory chain) internal pure returns (bytes32) {
92 | return keccak256(abi.encode(CHAIN_TYPEHASH, chain.assetRecoveryAddress, hash(chain.accounts), chain.id));
93 | }
94 |
95 | function hash(Account[] memory accounts) internal pure returns (bytes32) {
96 | // Array values are encoded as the keccak256 of the concatenation of the encoded values.
97 | // https://eips.ethereum.org/EIPS/eip-712#definition-of-encodedata
98 | bytes memory encoded;
99 | for (uint256 i = 0; i < accounts.length; i++) {
100 | encoded = abi.encodePacked(encoded, hash(accounts[i]));
101 | }
102 |
103 | return keccak256(encoded);
104 | }
105 |
106 | function hash(Account memory account) internal pure returns (bytes32) {
107 | // Account signatures are not included in the hash, avoiding circular dependancies.
108 | bytes32 empty = keccak256(new bytes(0));
109 | return keccak256(abi.encode(ACCOUNT_TYPEHASH, account.accountAddress, account.childContractScope, empty));
110 | }
111 |
112 | // function hash(ChildContractScope childContractScope) internal pure returns (bytes32) {
113 | // if (childContractScope == ChildContractScope.None) {
114 | // return hash("None");
115 | // } else if (childContractScope == ChildContractScope.ExistingOnly) {
116 | // return hash("ExistingOnly");
117 | // } else if (childContractScope == ChildContractScope.All) {
118 | // return hash("All");
119 | // } else {
120 | // revert("Invalid child contract scope");
121 | // }
122 | // }
123 |
124 | function hash(BountyTerms memory bountyTerms) internal pure returns (bytes32) {
125 | return keccak256(
126 | abi.encode(
127 | BOUNTYTERMS_TYPEHASH,
128 | bountyTerms.bountyPercentage,
129 | bountyTerms.bountyCapUSD,
130 | bountyTerms.retainable,
131 | bountyTerms.identity,
132 | hash(bountyTerms.diligenceRequirements)
133 | )
134 | );
135 | }
136 |
137 | // function hash(IdentityRequirements identity) internal pure returns (bytes32) {
138 | // if (identity == IdentityRequirements.Anonymous) {
139 | // return hash("Anonymous");
140 | // } else if (identity == IdentityRequirements.Pseudonymous) {
141 | // return hash("Pseudonymous");
142 | // } else if (identity == IdentityRequirements.Named) {
143 | // return hash("Named");
144 | // } else {
145 | // revert("Invalid identity");
146 | // }
147 | // }
148 |
149 | function hash(string memory str) internal pure returns (bytes32) {
150 | return keccak256(bytes(str));
151 | }
152 | }
153 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | # SEAL Whitehat Safe Harbor Agreement
6 |
7 | The Whitehat Safe Harbor initiative is a framework in which protocols can offer legal protection to whitehats who aid in the recovery of assets during an active exploit.
8 |
9 | ## What's in this repo?
10 |
11 | - [documents/agreement.pdf](documents/agreement.pdf) - the key legal document that defines the framework.
12 | - [documents/summary.pdf](documents/summary.pdf) - a helper document that summarizes the official agreement.
13 | - [documents/FAQ.md](documents/FAQ.md) - answers to common questions about SEAL Safe Harbor.
14 | - [registry-contracts/](registry-contracts/) - the smart contracts that on-chain governance can use to signal their official adoption of the agreement.
15 | - [releases](https://github.com/security-alliance/safe-harbor/releases) - release changelog for different versions of the seal whitehat safe harbor agreement.
16 |
17 |
18 | ## Registry Addresses
19 |
20 | | Chain | Address | Version |
21 | | ------------- | -------------------------------------------------------------------------------------------------------------------------------- | ------- |
22 | | Ethereum | [0x1eaCD100B0546E433fbf4d773109cAD482c34686](https://etherscan.io/address/0x1eaCD100B0546E433fbf4d773109cAD482c34686) | 1.1 |
23 | | BSC | [0x1eaCD100B0546E433fbf4d773109cAD482c34686](https://bscscan.com/address/0x1eaCD100B0546E433fbf4d773109cAD482c34686) | 1.1 |
24 | | Polygon | [0x1eaCD100B0546E433fbf4d773109cAD482c34686](https://polygonscan.com/address/0x1eaCD100B0546E433fbf4d773109cAD482c34686) | 1.1 |
25 | | | | |
26 | | Ethereum | [0x8f72fcf695523A6FC7DD97EafDd7A083c386b7b6](https://etherscan.io/address/0x8f72fcf695523A6FC7DD97EafDd7A083c386b7b6) | 1 |
27 | | Polygon | [0x8f72fcf695523A6FC7DD97EafDd7A083c386b7b6](https://polygonscan.com/address/0x8f72fcf695523A6FC7DD97EafDd7A083c386b7b6) | 1 |
28 | | Arbitrum | [0x8f72fcf695523A6FC7DD97EafDd7A083c386b7b6](https://arbiscan.io/address/0x8f72fcf695523A6FC7DD97EafDd7A083c386b7b6) | 1 |
29 | | Optimism | [0x8f72fcf695523A6FC7DD97EafDd7A083c386b7b6](https://optimistic.etherscan.io/address/0x8f72fcf695523A6FC7DD97EafDd7A083c386b7b6) | 1 |
30 | | Base | [0x8f72fcf695523A6FC7DD97EafDd7A083c386b7b6](https://basescan.org/address/0x8f72fcf695523A6FC7DD97EafDd7A083c386b7b6) | 1 |
31 | | Avalanche C | [0x8f72fcf695523A6FC7DD97EafDd7A083c386b7b6](https://snowtrace.io/address/0x8f72fcf695523A6FC7DD97EafDd7A083c386b7b6) | 1 |
32 | | Polygon zkEVM | [0x8f72fcf695523A6FC7DD97EafDd7A083c386b7b6](https://zkevm.polygonscan.com/address/0x8f72fcf695523A6FC7DD97EafDd7A083c386b7b6) | 1 |
33 | | BSC | [0x8f72fcf695523A6FC7DD97EafDd7A083c386b7b6](https://bscscan.com/address/0x8f72fcf695523a6fc7dd97eafdd7a083c386b7b6) | 1 |
34 | | Gnosis | [0x8f72fcf695523A6FC7DD97EafDd7A083c386b7b6](https://gnosisscan.io/address/0x8f72fcf695523a6fc7dd97eafdd7a083c386b7b6) | 1 |
35 | | ZKsync | [0x5f5eEc1a37F42883Df9DacdAb11985467F813877](https://explorer.zksync.io/address/0x5f5eEc1a37F42883Df9DacdAb11985467F813877) | 1 |
36 |
37 | ## Factory Addresses
38 |
39 | | Chain | Address | Version |
40 | | -------- | ------------------------------------------------------------------------------------------------------------------------ | ------- |
41 | | Ethereum | [0x98D1594Ba4f2115f75392ac92A7e3C8A81C67Fed](https://etherscan.io/address/0x98D1594Ba4f2115f75392ac92A7e3C8A81C67Fed) | 1.1 |
42 | | BSC | [0x98D1594Ba4f2115f75392ac92A7e3C8A81C67Fed](https://bscscan.com/address/0x98D1594Ba4f2115f75392ac92A7e3C8A81C67Fed) | 1.1 |
43 | | Polygon | [0x98D1594Ba4f2115f75392ac92A7e3C8A81C67Fed](https://polygonscan.com/address/0x98D1594Ba4f2115f75392ac92A7e3C8A81C67Fed) | 1.1 |
44 |
45 | ## How does it work?
46 |
47 | The Safe Harbor initiative is a preemptive security measure for protocols, similar to a bug bounty. It is a framework specifically for _active exploits_, i.e. situations where a vulnerability has begun to be exploited by a malicious actor. If a protocol has adopted Safe Harbor before such an incident occurs, whitehats will have clarity on how to act in a potential rescue, and will be more likely to help intervene.
48 |
49 | ### Protocol adoption
50 |
51 | If a protocol has reviewed the agreement, weighed its pros and cons, and is interested in proceeding with adoption, a few steps are required.
52 |
53 | Firstly, a decision must be made regarding the agreement's terms, including:
54 |
55 | - Which assets are in-scope for the agreement (e.g. any ERC20 token at a specific address)?
56 | - What reward will be given to successful whitehat rescues (e.g. 10% of rescued funds capped at $1m)?
57 | - Where should rescued funds be returned (e.g. a specific multisig or treasury address)?
58 |
59 | Once the specifics are determined, a governance proposal should be created for voting on the adoption of Safe Harbor. Exhibit B of the agreement provides details on how this proposal should be structured. For transparency and future on-chain referencing, all relevant documents should be uploaded to IPFS at this stage. If the protocol doesn't have an official on-chain voting procedure, alternative methods can be explored to engage the community in the decision-making process.
60 |
61 | If the decision to adopt Safe Harbor becomes official, there are three final steps for adoption:
62 |
63 | - An "Agreement Fact Page" must be created by the protocol. This provides all information about the protocol's adoption of the agreement, and must be maintained off-chain for anyone to view.
64 | - The "User Adoption Procedures" (Exhibit D of the agreement) must be adapted and inserted into the protocol website's terms-of-service.
65 | - A governance address must send an on-chain transaction to the Safe Harbor registry contract. This is the legally binding action, so the address calling the registry should represent the decision-making authority of the protocol.
66 |
67 | ### Whitehat adoption
68 |
69 | If a whitehat reads and understands the entire legal framework, they may later be eligible to participate in a whitehat rescue. These rescues should only be taken in very specific circumstances, and it is important to reiterate the following:
70 |
71 | - The framework only applies to _active exploits_, and it is a violation of the agreement if the whitehat initiates an exploit themselves.
72 | - The protocol is not responsible for ensuring the whitehat follows the law, and the whitehat can not be protected from criminal charges outside the agreement's scope.
73 | - There are nuances that can affect the agreement's enforceability, and whitehats will assume many legal risks by becoming involved.
74 |
75 | If the whitehat decides to proceed with a whitehat rescue, they must follow the process specified in the agreement. This includes transferring rescued funds to the protocol's "Asset Recovery Address" and promptly notifying the protocol of the fund recovery. The whitehat may keep (or later receive) a reward, based on the terms of the agreement.
76 |
77 | ## Diagram
78 |
79 | 
80 |
81 | ## How to Adopt Safe Harbor
82 |
83 | To find out more information about adopting Safe Harbor, please check out the [Safe Harbor SEAL Framework](https://frameworks.securityalliance.org/safe-harbor/index.html). Or reach out to us at [safe-harbor@securityalliance.org](mailto:safe-harbor@securityalliance.org).
84 |
85 |
86 | # Solana programs
87 | To clone repo with solana programs
88 | ```bash
89 | $ git clone git@github.com:security-alliance/safe-harbor.git
90 | $ cd safe-harbor
91 | $ git submodule update --init --recursive
92 | ```
93 |
94 | or
95 |
96 | ```sh
97 | $ git clone --recursive git@github.com:security-alliance/safe-harbor.git
98 | ```
--------------------------------------------------------------------------------
/registry-contracts/script/v2/DeployRegistryV2.s.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: UNLICENSED
2 | pragma solidity ^0.8.13;
3 |
4 | import {Script} from "forge-std/Script.sol";
5 | import {console} from "forge-std/console.sol";
6 | import {SafeHarborRegistryV2, IRegistry} from "../../src/v2/SafeHarborRegistryV2.sol";
7 | import {AgreementFactoryV2} from "../../src/v2/AgreementFactoryV2.sol";
8 |
9 | contract DeployRegistryV2 is Script {
10 | // This is a create2 factory deployed by a one-time-use-account as described here:
11 | // https://github.com/Arachnid/deterministic-deployment-proxy. As a result, this factory
12 | // exists (or can exist) on any EVM compatible chain, and gives us a guaranteed way to deploy
13 | // the registry to a deterministic address across all chains. This is used by default in foundry
14 | // when we specify a salt in a contract creation.
15 | address constant DETERMINISTIC_CREATE2_FACTORY = 0x4e59b44847b379578588920cA78FbF26c0B4956C;
16 |
17 | // This could have been any value, but we choose zero.
18 | bytes32 constant DETERMINISTIC_DEPLOY_SALT = bytes32(0);
19 |
20 | address constant EXPECTED_DEPLOYER_ADDRESS = 0x31d23affb90bCAfcAAe9f27903b151DCDC82569E;
21 |
22 | // This is the address of the fallback registry that has already been deployed.
23 | // Set this to the zero address if no fallback registry exists.
24 | address fallbackRegistry = getFallbackRegistryAddress();
25 |
26 | function run() public {
27 | require(
28 | DETERMINISTIC_CREATE2_FACTORY.code.length != 0,
29 | "Create2 factory not deployed yet, see https://github.com/Arachnid/deterministic-deployment-proxy."
30 | );
31 |
32 | if (fallbackRegistry == address(0)) {
33 | console.log("WARNING: Deploying SafeHarborRegistryV2 with no fallback registry.");
34 | } else {
35 | // Disable this check if you want to deploy the register with no fallback registry.
36 | require(fallbackRegistry.code.length > 0, "No contract exists at the fallback registry address.");
37 | }
38 |
39 | uint256 deployerPrivateKey = vm.envUint("REGISTRY_DEPLOYER_PRIVATE_KEY");
40 | address deployerAddress = vm.addr(deployerPrivateKey);
41 |
42 | if (deployerAddress != EXPECTED_DEPLOYER_ADDRESS) {
43 | console.log("WARNING: The deployer address does not match the expected address.");
44 | console.log("Expected deployer address:");
45 | console.logAddress(EXPECTED_DEPLOYER_ADDRESS);
46 | console.log("Actual deployer address:");
47 | console.logAddress(deployerAddress);
48 | }
49 |
50 | console.log("Deploying from");
51 | console.logAddress(deployerAddress);
52 |
53 | // Deploy the Registry
54 | address expectedRegistryAddress = getExpectedRegistryAddress(deployerAddress);
55 | if (expectedRegistryAddress.code.length == 0) {
56 | deployRegistry(deployerPrivateKey, fallbackRegistry, deployerAddress, expectedRegistryAddress);
57 | } else {
58 | console.log("Registry already deployed at:");
59 | console.logAddress(expectedRegistryAddress);
60 | }
61 |
62 | // Deploy the Factory
63 | address expectedFactoryAddress = getExpectedFactoryAddress();
64 | if (expectedFactoryAddress.code.length == 0) {
65 | deployFactory(deployerPrivateKey, expectedFactoryAddress);
66 | } else {
67 | console.log("Factory already deployed at:");
68 | console.logAddress(expectedFactoryAddress);
69 | }
70 | }
71 |
72 | function deployRegistry(
73 | uint256 deployerPrivateKey,
74 | address _fallbackRegistry,
75 | address _owner,
76 | address expectedAddress
77 | ) internal {
78 | vm.broadcast(deployerPrivateKey);
79 | SafeHarborRegistryV2 registry = new SafeHarborRegistryV2{salt: DETERMINISTIC_DEPLOY_SALT}(_owner);
80 |
81 | if (_fallbackRegistry != address(0)) {
82 | vm.broadcast(deployerPrivateKey);
83 | registry.setFallbackRegistry(IRegistry(_fallbackRegistry));
84 | }
85 |
86 | address deployedRegistryAddress = address(registry);
87 |
88 | require(
89 | deployedRegistryAddress == expectedAddress,
90 | "Deployed to unexpected address. Check that Foundry is using the correct create2 factory."
91 | );
92 |
93 | require(
94 | deployedRegistryAddress.code.length != 0,
95 | "Registry deployment failed. Check that Foundry is using the correct create2 factory."
96 | );
97 |
98 | console.log("SafeHarborRegistryV2 deployed to:");
99 | console.logAddress(deployedRegistryAddress);
100 | }
101 |
102 | function deployFactory(uint256 deployerPrivateKey, address expectedAddress) internal {
103 | vm.broadcast(deployerPrivateKey);
104 | AgreementFactoryV2 factory = new AgreementFactoryV2{salt: DETERMINISTIC_DEPLOY_SALT}();
105 |
106 | address deployedFactoryAddress = address(factory);
107 |
108 | require(
109 | deployedFactoryAddress == expectedAddress,
110 | "Factory deployed to unexpected address. Check that Foundry is using the correct create2 factory."
111 | );
112 |
113 | require(
114 | deployedFactoryAddress.code.length != 0,
115 | "Factory deployment failed. Check that Foundry is using the correct create2 factory."
116 | );
117 |
118 | console.log("AgreementFactoryV2 deployed to:");
119 | console.logAddress(deployedFactoryAddress);
120 | }
121 |
122 | function getFallbackRegistryAddress() internal view returns (address) {
123 | uint256 chainId = block.chainid;
124 |
125 | // Most chains use this address
126 | address standardFallback = 0x8f72fcf695523A6FC7DD97EafDd7A083c386b7b6;
127 |
128 | // Map chain IDs to their fallback registry addresses
129 | if (chainId == 1) return standardFallback; // Ethereum
130 | if (chainId == 137) return standardFallback; // Polygon
131 | if (chainId == 42161) return standardFallback; // Arbitrum
132 | if (chainId == 10) return standardFallback; // Optimism
133 | if (chainId == 8453) return standardFallback; // Base
134 | if (chainId == 43114) return standardFallback; // Avalanche C
135 | if (chainId == 1101) return standardFallback; // Polygon zkEVM
136 | if (chainId == 56) return standardFallback; // BSC
137 | if (chainId == 100) return standardFallback; // Gnosis
138 | if (chainId == 324) return 0x5f5eEc1a37F42883Df9DacdAb11985467F813877; // ZKsync
139 |
140 | // For any other chain, return zero address (no fallback registry)
141 | return address(0);
142 | }
143 |
144 | // Computes the address which the registry will be deployed to, assuming the correct create2 factory
145 | // and salt are used.
146 | function getExpectedRegistryAddress(address _owner) public pure returns (address) {
147 | return address(
148 | uint160(
149 | uint256(
150 | keccak256(
151 | abi.encodePacked(
152 | bytes1(0xff),
153 | DETERMINISTIC_CREATE2_FACTORY,
154 | DETERMINISTIC_DEPLOY_SALT,
155 | keccak256(abi.encodePacked(type(SafeHarborRegistryV2).creationCode, abi.encode(_owner)))
156 | )
157 | )
158 | )
159 | )
160 | );
161 | }
162 |
163 | // Computes the address which the factory will be deployed to
164 | function getExpectedFactoryAddress() public pure returns (address) {
165 | return address(
166 | uint160(
167 | uint256(
168 | keccak256(
169 | abi.encodePacked(
170 | bytes1(0xff),
171 | DETERMINISTIC_CREATE2_FACTORY,
172 | DETERMINISTIC_DEPLOY_SALT,
173 | keccak256(abi.encodePacked(type(AgreementFactoryV2).creationCode))
174 | )
175 | )
176 | )
177 | )
178 | );
179 | }
180 | }
181 |
--------------------------------------------------------------------------------
/registry-contracts/README.md:
--------------------------------------------------------------------------------
1 | # Safe Harbor Registry
2 |
3 | This directory houses the "Safe Harbor Registry". This is a smart contract written in Solidity which serves three main purposes:
4 |
5 | 1. Allow protocols to officially adopt the SEAL Whitehat Safe Harbor Agreement.
6 | 2. Store the agreement details on-chain as a permanent record.
7 | 3. Allow for future updates to the agreement terms by adopters.
8 |
9 | These registry contracts were designed for EVM-compatible chains. For non-EVM chains, new registry contracts may need to be written and seperately deployed.
10 |
11 | # Technical Details
12 |
13 | This repository is built using [Foundry](https://book.getfoundry.sh/). See the installation instructions [here](https://github.com/foundry-rs/foundry#installation). To test the contracts, use `forge test`.
14 |
15 | There are 2 active versions in this system:
16 |
17 | - `src/v2/SafeHarborRegistryV2.sol` and `src/v2/AgreementV2.sol` (current)
18 | - `src/v1/SafeHarborRegistry.sol` and `src/v1/AgreementV1.sol` (legacy)
19 |
20 | ## Setup
21 |
22 | 1. The `SafeHarborRegistry` contract is deployed with the fallback registry as constructor arguments.
23 |
24 | In the future SEAL may create new versions of this agreement. When this happens a new registry (e.g. `SafeHarborRegistryV2`) may be deployed. New registries will fallback to prior registries, so the latest deployed registry will act as the source of truth for all adoption details. Old registries will always remain functional.
25 |
26 | ## Adoption
27 |
28 | 1. A protocol creates their agreement details contract using one of the provided `AgreementFactories`. This can be done using any address.
29 | 2. A protocol calls `adoptSafeHarbor()` on a `SafeHarborRegistry` with their agreement contract. This must be done from a legally representative address of that protocol.
30 | 3. The registry records the adopted `Agreement` address as an adoption by `msg.sender`.
31 |
32 | A protocol may update their agreement details using any enabled registry. To do so, the protocol calls `adoptSafeHarbor()` on an agreement registry with their new agreement details. This will create a new `Agreement` contract and store it as the details for `msg.sender`. Protocols may also update their details on any mutable Agreement.
33 |
34 | Calling `adoptSafeHarbor()` is considered the legally binding action. The `msg.sender` should represent the decision-making authority of the protocol.
35 |
36 | ### Using the script to adopt Safe Harbor
37 |
38 | #### V2 Adoption (Recommended)
39 |
40 | The V2 adoption script supports configurable options and is the recommended approach for new adoptions.
41 |
42 | **Environment Variables:**
43 |
44 | | Variable | Required | Default | Description |
45 | |----------|----------|---------|-------------|
46 | | `DEPLOYER_PRIVATE_KEY` | ✅ | - | Private key for transaction signing |
47 | | `AGREEMENT_OWNER` | ❌ | Deployer address | Address that will own the agreement |
48 | | `ADOPT_TO_REGISTRY` | ❌ | `false` | Whether to adopt the agreement to the registry |
49 |
50 | **Usage:**
51 | ```bash
52 | cd registry-contracts
53 | forge script script/v2/AdoptSafeHarborV2.s.sol:AdoptSafeHarborV2 --rpc-url --verify --broadcast
54 | ```
55 |
56 | **Configuration:** The script reads agreement details from `agreementDetailsV2.json`. Make sure this file exists and contains valid agreement configuration before running the script.
57 |
58 | #### V1 Adoption (Legacy)
59 |
60 | For V1 adoptions:
61 | 1. Edit agreementDetails.json with the agreement details of your protocol.
62 | 2. Create a .env file and set the `DEPLOYER_PRIVATE_KEY` environment variable.
63 | 3. Run the script using:
64 |
65 | ```bash
66 | forge script script/v1/AdoptSafeHarborV1.s.sol:AdoptSafeHarborV1 --rpc-url --verify --broadcast
67 | ```
68 |
69 | If you would like to deploy from the protocol multisig, please contact us directly.
70 |
71 | ### V2 Utilities
72 |
73 | #### Add chains to an existing AgreementV2
74 |
75 | The add-chains script reads from `addChainsV2.json` and calls `AgreementV2.addChains`.
76 |
77 | **Requirements:**
78 |
79 | - `DEPLOYER_PRIVATE_KEY` in env (must be the current `owner()` of the agreement)
80 | - `addChainsV2.json` in the repo root with fields:
81 | - `agreementAddress` (checksummed address)
82 | - `chains[]` objects with `id` (CAIP-2), `assetRecoveryAddress`, `accounts[]` where each account has `accountAddress` and `childContractScope` (enum as uint)
83 |
84 | **Usage:**
85 |
86 | ```bash
87 | cd registry-contracts
88 | forge script script/v2/AddChainsV2.s.sol:AddChainsV2 --rpc-url --broadcast
89 | ```
90 |
91 | #### Change AgreementV2 owner
92 |
93 | Transfer ownership of an `AgreementV2` to a new address.
94 |
95 | **CLI (recommended):**
96 |
97 | ```bash
98 | cd registry-contracts
99 | forge script script/v2/ChangeOwnerV2.s.sol:ChangeOwnerV2 \
100 | --sig 'run(address,address)' 0xAgreementAddress 0xNewOwnerAddress \
101 | --rpc-url --broadcast
102 | ```
103 |
104 | **Env-based:**
105 |
106 | ```bash
107 | export DEPLOYER_PRIVATE_KEY=0x...
108 | export AGREEMENT_ADDRESS=0xAgreementAddress
109 | export NEW_OWNER=0xNewOwnerAddress
110 | cd registry-contracts
111 | forge script script/v2/ChangeOwnerV2.s.sol:ChangeOwnerV2 --rpc-url --broadcast
112 | ```
113 |
114 | #### Get AgreementV2 details
115 |
116 | Logs the current `AgreementDetailsV2` to the console.
117 |
118 | **CLI:**
119 |
120 | ```bash
121 | cd registry-contracts
122 | forge script script/v2/GetAgreementDetailsV2.s.sol:GetAgreementDetailsV2 \
123 | --sig 'run(address)' 0xAgreementAddress \
124 | --rpc-url
125 | ```
126 |
127 | **Env-based:**
128 |
129 | ```bash
130 | export AGREEMENT_ADDRESS=0xAgreementAddress
131 | cd registry-contracts
132 | forge script script/v2/GetAgreementDetailsV2.s.sol:GetAgreementDetailsV2 --rpc-url
133 | ```
134 |
135 | ### Signed Accounts
136 |
137 | For added security, protocols may choose to sign their agreement for the scoped accounts. Both EOA and ERC-1271 signatures are supported and can be validated with the registry. Given a signed account, whitehats can be certain that the owner of the account has approved the agreement details.
138 |
139 | `AccountDetails` use EIP-712 hashing for a better client-side experience.
140 |
141 | #### Verification of Signed Accounts
142 |
143 | Whitehats may use the registy's `validateAccount()` method to verify that a given Account has consented to the agreement details.
144 |
145 | ## Querying Agreements
146 |
147 | 1. Query the `SafeHarborRegistry` contract with the protocol address to get the protocol's `AgreementV*` address.
148 | 2. Query the protocol's `Agreement` contract with `getDetails()` to get the address of the structured agreement details.
149 |
150 | Different versions may have different `AgreementDetails` structs. All `Agreement` and `SafeHarborRegistry` contracts will include a `version()` method which can be used to infer the `AgreementDetails` structure.
151 |
152 | If no agreement is present for a given query address in a registry, the registry will check the fallback registry provided in its constructor. This allows SEAL to deploy new registries while remaining backwards-compatible.
153 |
154 | # Deployment
155 |
156 | The Safe Harbor Registry will be deployed using the deterministic deployment proxy described here: https://github.com/Arachnid/deterministic-deployment-proxy, which is built into Foundry by default.
157 |
158 | To deploy the registry to an EVM-compatible chain where it is not currently deployed:
159 |
160 | 1. Ensure the deterministic-deployment-proxy is deployed at 0x4e59b44847b379578588920cA78FbF26c0B4956C, and if it's not, deploy it using [the process mentioned above](https://github.com/Arachnid/deterministic-deployment-proxy).
161 | 2. Deploy the registry using the above proxy with salt `bytes32(0)` from the EOA that will become the registry admin. The file [`script/v1/DeployRegistryV1.s.sol`](script/v1/DeployRegistryV1.s.sol) is a convenience script for this task. To use it, set the `REGISTRY_DEPLOYER_PRIVATE_KEY` environment variable to a private key that can pay for the deployment transaction costs, or use `--ledger` to deploy with a ledger account. Then, run the script using:
162 |
163 | ```
164 | cd registry-contracts
165 | forge script script/v1/DeployRegistryV1.s.sol:DeployRegistryV1 --rpc-url --verify --broadcast --ledger
166 | ```
167 |
168 | *https://chainlist.org*
169 |
--------------------------------------------------------------------------------
/registry-contracts/src/v2/AgreementV2.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity ^0.8.20;
3 |
4 | import {console} from "forge-std/console.sol";
5 | import {
6 | AgreementDetailsV2,
7 | Chain,
8 | Account,
9 | Contact,
10 | BountyTerms,
11 | ChildContractScope,
12 | IdentityRequirements
13 | } from "./AgreementDetailsV2.sol";
14 | import {SafeHarborRegistryV2, VERSION} from "./SafeHarborRegistryV2.sol";
15 | import "openzeppelin-contracts/contracts/access/Ownable.sol";
16 |
17 | /// @notice Contract that contains the AgreementDetails that will be deployed by the Agreement Factory.
18 | /**
19 | * @dev
20 | * This contract is Ownable and mutable. It is intended to be used by entities adopting the
21 | * Safe Harbor agreement that either need to frequently update their terms, have too many terms to
22 | * fit in a single transaction, or wish to delegate the management of their agreement to a different
23 | * address than the deployer.
24 | */
25 | contract AgreementV2 is Ownable {
26 | // ----- STATE VARIABLES -----
27 |
28 | /// @notice The details of the agreement.
29 | AgreementDetailsV2 private details;
30 |
31 | /// @notice The Safe Harbor Registry V2 contract
32 | SafeHarborRegistryV2 private registry;
33 |
34 | /// @notice Temporary mapping used for duplicate chain ID validation
35 | mapping(bytes32 => bool) private _tempChainIdSeen;
36 |
37 | // ----- EVENTS -----
38 |
39 | /// @notice An event that records when a safe harbor agreement is updated.
40 | event AgreementUpdated();
41 |
42 | // ----- ERRORS -----
43 |
44 | error ChainNotFound();
45 | error AccountNotFound();
46 | error CannotSetBothAggregateBountyCapUSDAndRetainable();
47 | error ChainNotFoundByCaip2Id(string caip2ChainId);
48 | error AccountNotFoundByAddress(string caip2ChainId, string accountAddress);
49 | error DuplicateChainId(string caip2ChainId);
50 | error InvalidChainId(string caip2ChainId);
51 |
52 | // ----- CONSTRUCTOR -----
53 |
54 | /// @notice Constructor that sets the details of the agreement.
55 | /// @param _details The details of the agreement.
56 | /// @param _registry The address of the Safe Harbor Registry V2 contract
57 | /// @param _owner The owner of the agreement
58 | constructor(AgreementDetailsV2 memory _details, address _registry, address _owner) Ownable(_owner) {
59 | registry = SafeHarborRegistryV2(_registry);
60 | _validateBountyTerms(_details.bountyTerms);
61 | _validateNoDuplicateChainIds(_details.chains);
62 | _validateChainIds(_details.chains);
63 | details = _details;
64 | }
65 |
66 | // ----- EXTERNAL FUNCTIONS -----
67 |
68 | /// @notice Function that sets the protocol name
69 | function setProtocolName(string memory _protocolName) external onlyOwner {
70 | details.protocolName = _protocolName;
71 | emit AgreementUpdated();
72 | }
73 |
74 | /// @notice Function that sets the agreement contact details.
75 | function setContactDetails(Contact[] memory _contactDetails) external onlyOwner {
76 | details.contactDetails = _contactDetails;
77 | emit AgreementUpdated();
78 | }
79 |
80 | /// @notice Function that adds multiple chains to the agreement.
81 | function addChains(Chain[] memory _chains) external onlyOwner {
82 | _validateChainIds(_chains);
83 | for (uint256 i = 0; i < _chains.length; i++) {
84 | details.chains.push(_chains[i]);
85 | }
86 | _validateNoDuplicateChainIds(details.chains);
87 |
88 | emit AgreementUpdated();
89 | }
90 |
91 | /// @notice Function that sets multiple chains in the agreement, keeping existing chains.
92 | /// @dev This function replaces the existing chains with the new ones.
93 | function setChains(Chain[] memory _chains) external onlyOwner {
94 | for (uint256 i = 0; i < _chains.length; i++) {
95 | uint256 chainIndex = _findChainIndex(_chains[i].caip2ChainId);
96 | details.chains[chainIndex] = _chains[i];
97 | }
98 |
99 | emit AgreementUpdated();
100 | }
101 |
102 | /// @notice Removes multiple chains from the agreement by CAIP-2 IDs.
103 | /// @param _caip2ChainIds Array of CAIP-2 IDs of the chains to remove
104 | function removeChains(string[] memory _caip2ChainIds) external onlyOwner {
105 | for (uint256 i = 0; i < _caip2ChainIds.length; i++) {
106 | uint256 chainIndex = _findChainIndex(_caip2ChainIds[i]);
107 | details.chains[chainIndex] = details.chains[details.chains.length - 1];
108 | details.chains.pop();
109 | }
110 | emit AgreementUpdated();
111 | }
112 |
113 | /// @notice Function that adds multiple accounts to the agreement.
114 | /// @param _caip2ChainId The CAIP-2 ID of the chain
115 | /// @param _accounts Array of accounts to add
116 | function addAccounts(string memory _caip2ChainId, Account[] memory _accounts) external onlyOwner {
117 | uint256 chainIndex = _findChainIndex(_caip2ChainId);
118 |
119 | for (uint256 i = 0; i < _accounts.length; i++) {
120 | details.chains[chainIndex].accounts.push(_accounts[i]);
121 | }
122 |
123 | emit AgreementUpdated();
124 | }
125 |
126 | /// @notice Function that removes multiple accounts from the agreement by addresses.
127 | /// @param _caip2ChainId The CAIP-2 ID of the chain containing the accounts
128 | /// @param _accountAddresses Array of account addresses to remove
129 | function removeAccounts(string memory _caip2ChainId, string[] memory _accountAddresses) external onlyOwner {
130 | uint256 chainIndex = _findChainIndex(_caip2ChainId);
131 | for (uint256 i = 0; i < _accountAddresses.length; i++) {
132 | uint256 accountIndex = _findAccountIndex(chainIndex, _accountAddresses[i]);
133 |
134 | uint256 lastAccountId = details.chains[chainIndex].accounts.length - 1;
135 | details.chains[chainIndex].accounts[accountIndex] = details.chains[chainIndex].accounts[lastAccountId];
136 | details.chains[chainIndex].accounts.pop();
137 | }
138 | emit AgreementUpdated();
139 | }
140 |
141 | /// @notice Function that sets the bounty terms of the agreement.
142 | function setBountyTerms(BountyTerms memory _bountyTerms) external onlyOwner {
143 | _validateBountyTerms(_bountyTerms);
144 | details.bountyTerms = _bountyTerms;
145 | emit AgreementUpdated();
146 | }
147 |
148 | /// @notice Function that returns the agreement details
149 | /// @dev You need a view function, else it won't convert storage to memory automatically for the nested structs.
150 | function getDetails() external view returns (AgreementDetailsV2 memory) {
151 | return details;
152 | }
153 |
154 | // ----- INTERNAL FUNCTIONS -----
155 |
156 | /// @notice Internal function to validate that chains don't have duplicate CAIP-2 IDs
157 | function _validateNoDuplicateChainIds(Chain[] memory _chains) internal {
158 | // Clean up the temporary mapping
159 | for (uint256 i = 0; i < _chains.length; i++) {
160 | bytes32 chainIdHash = keccak256(bytes(_chains[i].caip2ChainId));
161 | delete _tempChainIdSeen[chainIdHash];
162 | }
163 |
164 | // Check for duplicates
165 | for (uint256 i = 0; i < _chains.length; i++) {
166 | bytes32 chainIdHash = keccak256(bytes(_chains[i].caip2ChainId));
167 | if (_tempChainIdSeen[chainIdHash]) {
168 | revert DuplicateChainId(_chains[i].caip2ChainId);
169 | }
170 | _tempChainIdSeen[chainIdHash] = true;
171 | }
172 | }
173 |
174 | /// @notice Internal function to validate that all chain IDs in the agreement are valid
175 | function _validateChainIds(Chain[] memory _chains) internal view {
176 | for (uint256 i = 0; i < _chains.length; i++) {
177 | if (!registry.isChainValid(_chains[i].caip2ChainId)) {
178 | revert InvalidChainId(_chains[i].caip2ChainId);
179 | }
180 | }
181 | }
182 |
183 | /// @notice Internal function to validate bounty terms
184 | function _validateBountyTerms(BountyTerms memory _bountyTerms) internal pure {
185 | if (_bountyTerms.aggregateBountyCapUSD > 0 && _bountyTerms.retainable) {
186 | revert CannotSetBothAggregateBountyCapUSDAndRetainable();
187 | }
188 | }
189 |
190 | /// @notice Internal function to find chain index by CAIP-2 ID
191 | function _findChainIndex(string memory _caip2ChainId) internal view returns (uint256 chainIndex) {
192 | for (uint256 i = 0; i < details.chains.length; i++) {
193 | if (keccak256(bytes(details.chains[i].caip2ChainId)) == keccak256(bytes(_caip2ChainId))) {
194 | return i;
195 | }
196 | }
197 | revert ChainNotFoundByCaip2Id(_caip2ChainId);
198 | }
199 |
200 | /// @notice Internal function to find account index by address within a chain
201 | function _findAccountIndex(uint256 _chainIndex, string memory _accountAddress)
202 | internal
203 | view
204 | returns (uint256 accountIndex)
205 | {
206 | for (uint256 i = 0; i < details.chains[_chainIndex].accounts.length; i++) {
207 | if (
208 | keccak256(bytes(details.chains[_chainIndex].accounts[i].accountAddress))
209 | == keccak256(bytes(_accountAddress))
210 | ) {
211 | return i;
212 | }
213 | }
214 | revert AccountNotFoundByAddress(details.chains[_chainIndex].caip2ChainId, _accountAddress);
215 | }
216 | }
217 |
--------------------------------------------------------------------------------
/documents/exhibits/f.tex:
--------------------------------------------------------------------------------
1 | \subsubsection*{Safe Harbor Agreement for Whitehats - Frequently Asked Questions}\label{exhibit:f:faq_title}
2 |
3 | This document ("FAQ") is meant to provide additional information to Protocol Communities about certain aspects of the Safe Harbor Agreement for Whitehats ("Agreement"). In the event of any conflict or inconsistency between the FAQ and the text of the Agreement, the text of the Agreement will govern. The information provided in the FAQ does not, and is not intended to, constitute legal advice.
4 |
5 | \paragraph{Adoption and Initial Implementation}\label{exhibit:f:adoption}
6 |
7 | \begin{enumerate}
8 | \item \textbf{What is a Protocol Community?} As defined in the Agreement, a Protocol Community is the set of key stakeholders with an interest in a blockchain-based protocol or similar decentralized technology. This group will typically include the DAO governing a protocol, DAO members and participants, protocol users, and any individual or group of individuals involved in securing the protocol.
9 |
10 | \item \textbf{If most blockchain-based protocols are meant to be open and permissionless, then how does a Protocol Community adopt the Agreement?} Exhibits B, C, and D to the Agreement provide guidance on how various groups can adopt the Agreement. Given that decentralized technologies are being developed, governed, and used in new and innovative ways, the Security Alliance recommends that Protocol Communities are thoughtful about how they publicize, deliberate about, and adopt the Agreement. Protocol Communities should consult with legal counsel about the adoption process as needed. Protocol Communities should consider the possibility that individuals and entities involved in developing, governing, and using their protocol may each use different communication channels to coordinate and discuss the protocol. Protocol Communities should consider using all of these channels to provide these individuals and entities with opportunities to learn about the Agreement, discuss its adoption, and agree to its terms. For instance, Protocol Communities may want to consider using popular communication channels, like Twitter and Discord, to publicize the Agreement. Protocol Communities might also coordinate with any independent entities that provide user interfaces for their protocol to engage with users. For some Protocol Communities, these steps may be helpful for promoting engagement with the Agreement adoption process.
11 |
12 | \item \textbf{Should the process of adopting the Agreement occur in public?} Yes, Protocol Communities are required by the Agreement to create an Agreement Fact Page that provides access to the materials associated with the adoption process. Protocol Communities should consider making all aspects of the adoption process public so that as many stakeholders as possible can engage with the process.
13 |
14 | % COMMENT: The original document contains references to "Agreement Fact Page" and "Adoption Form" but these terms are not defined in the main agreement. These may need clarification or definition.
15 |
16 | \item \textbf{What steps should a Protocol Community take to implement the program described in the Agreement?} As described in the Agreement, Protocol Communities should take the following steps to adopt the Agreement and implement the program described in it:
17 |
18 | \begin{enumerate}[label=\alph*.]
19 | \item Protocol Communities should clearly disclose the parameters selected during the DAO Adoption Procedures. The Agreement is an open-source template that requires Protocol Communities to add certain details and make certain decisions before it is adopted. These required items include:
20 |
21 | \begin{enumerate}[label=\roman*.]
22 | \item Specifying the protocol or other decentralized technology that will be subject to the Agreement. This process might require drafting a list of technical assets that are within the Agreement's scope;
23 |
24 | % COMMENT: The original document references "Protocol Safety Address" but the main agreement uses "Asset Recovery Address". This inconsistency should be clarified.
25 | \item Indicating the specific Protocol Safety Address where Whitehats will deposit assets that they recover;
26 |
27 | \item Deciding whether to use a third-party vendor, like a bug bounty program administrator, to facilitate payment of the Bounty;
28 |
29 | \item Deciding whether anonymous or pseudonymous Whitehats can participate in the program and collect a Bounty without identifying themselves to the Protocol Community. This decision will impact the extent to which the Protocol Community can perform diligence on the Whitehat in advance of their participation in the program or collection of the Bounty;
30 |
31 | \item Deciding whether to perform sanctions diligence or other forms of diligence on Whitehats in advance of their participation in the program or their collection of the Bounty;
32 |
33 | \item Deciding the percentage of Returnable Assets to be paid to Eligible Whitehats as a Bounty, which may involve reviewing the payment amounts associated with a Protocol Community's existing bug bounty program, if any; and
34 |
35 | \item Deciding whether Whitehats will be permitted to deduct the Bounty themselves from the assets that they recover. This decision will limit the extent to which the Protocol Community can perform diligence on the Whitehat or assess their compliance with the Agreement before the Whitehat collects the Bounty.
36 | \end{enumerate}
37 |
38 | \item Protocol Communities may consider making additional determinations which, if made, should also be included in both the Adopting Addendum and on the Adoption Form. These additional determinations may include:
39 |
40 | % COMMENT: "Adopting Addendum" is referenced but not defined in the main agreement. This may need clarification.
41 |
42 | \begin{enumerate}[label=\roman*.]
43 | \item Deciding whether to impose any additional cap(s) on the Bounty paid in connection with an Urgent Blackhat Exploit, such as an aggregate cap equivalent to a US Dollar amount and above which payment will not be made to an Eligible Whitehat(s), or a fixed cap applicable to each Eligible Whitehat contributing to an Eligible Funds Rescue; and
44 |
45 | \item Incorporating other due diligence requirements on Whitehats that address the unique needs of the Protocol Community adopting the Agreement.
46 | \end{enumerate}
47 |
48 | \item Protocol Communities should consult with legal counsel in relevant jurisdictions about the specific legal risks and benefits of each of the choices described above because they may expose the Protocol Community or Protocol Community Members to legal or regulatory risk.
49 |
50 | \item As described above, Protocol Communities must make certain information about the adoption process publicly accessible. Protocol Communities should consider taking other steps to include different stakeholders in the process.
51 |
52 | \item Protocol Communities should also consider communicating to potential Whitehats whether there are any limits to the release provisions provided by the Agreement based on the Protocol Community's specific circumstances. For instance, a Protocol Community might take the position that the Agreement does not bind the Protocol's Users or other Protocol Community Members. Under that circumstance, the Protocol Community should consider notifying Whitehats that the release provisions might not protect them from claims brought by persons or entities who are not parties to the Agreement.
53 |
54 | \item Protocol Communities should consider the additional steps needed to implement the program. These steps may include, but are not limited to, coordinating with a bug bounty program administrator and creating internal organizational structures for administering the program.
55 | \end{enumerate}
56 | \end{enumerate}
57 |
58 | \paragraph{Compliance with Applicable Laws and Regulations}\label{exhibit:f:compliance}
59 |
60 | \begin{enumerate}
61 | \item \textbf{How can Protocol Communities adapt the Agreement so that it complies with applicable laws and regulations?} The Agreement is a template agreement that is meant to be adapted for use by sophisticated Protocol Communities around the world. Protocol Communities are encouraged to customize the parameters of the Agreement via the DAO Adoption Procedures parameter settings so that it conforms with the specific laws and regulations that apply to them and otherwise meets their particular needs.
62 |
63 | \item \textbf{Should Protocol Communities take any steps to ensure that their Bounty payments to Whitehats comply with international sanctions regimes?} Yes, each participating Protocol Community is expected to comply with applicable sanctions obligations, and the Security Alliance recommends that Protocol Communities implement a risk-based approach to ensuring compliance with these obligations. For example, while Section \ref{subsec:money_laundering} of the Agreement requires Whitehats to represent that they are not subject to any national or international sanctions regimes, in some jurisdictions, risk of sanctions violations may be increased where Whitehats are able to anonymously attempt an Eligible Funds Rescue and receive or retain Returnable Assets as a Bounty. This risk may also be heightened where the Protocol Community does not take other steps, such as conducting pre-payment diligence and instituting monitoring measures, to prevent payment to a sanctioned entity. The Security Alliance further recommends that Protocol Communities consult with legal counsel about how to address potential risks associated with the applicable sanctions regime(s) and to discuss what measures Protocol Communities may wish to take to comply with the applicable regime(s).
64 |
65 | \item \textbf{Should Protocol Communities make Whitehats aware of the risks associated with the Agreement and the program that it describes?} Yes. The Agreement includes a list of risk disclosures in Exhibit E. Protocol Communities should consider adding or modifying those risk disclosures to account for any risks that are specific to their situation. These specific risks might address positions that law enforcement or regulators may take with respect to the program in particular jurisdictions. Protocol Communities should consult with legal counsel about these risks as needed.
66 | \end{enumerate}
--------------------------------------------------------------------------------
/registry-contracts/test/v2/AgreementV2.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity ^0.8.20;
3 |
4 | import {console} from "forge-std/console.sol";
5 | import "forge-std/Test.sol" as Test;
6 | import "../../src/v2/AgreementV2.sol" as V2;
7 | import {SafeHarborRegistryV2} from "../../src/v2/SafeHarborRegistryV2.sol";
8 | import {getMockAgreementDetails} from "./mock.sol";
9 |
10 | contract AgreementV2Test is Test.Test {
11 | uint256 mockKey;
12 | address mockAddress;
13 | address owner;
14 |
15 | V2.AgreementDetailsV2 details;
16 | V2.AgreementV2 agreement;
17 | SafeHarborRegistryV2 registry;
18 |
19 | function setUp() public {
20 | mockKey = 0xA113;
21 | mockAddress = vm.addr(mockKey);
22 | owner = address(0x1);
23 |
24 | // Create registry and set valid chains
25 | registry = new SafeHarborRegistryV2(owner);
26 | string[] memory validChains = new string[](2);
27 | validChains[0] = "eip155:1";
28 | validChains[1] = "eip155:2";
29 | vm.prank(owner);
30 | registry.setValidChains(validChains);
31 |
32 | details = getMockAgreementDetails("0x01");
33 | agreement = new V2.AgreementV2(details, address(registry), owner);
34 | }
35 |
36 | function testOwner() public {
37 | assertEq(agreement.owner(), owner);
38 | assertFalse(agreement.owner() == address(0x02));
39 | }
40 |
41 | function testGetDetails() public {
42 | V2.AgreementDetailsV2 memory _details = agreement.getDetails();
43 | assertEq(keccak256(abi.encode(details)), keccak256(abi.encode(_details)));
44 | }
45 |
46 | function testSetProtocolName() public {
47 | string memory newName = "Updated Protocol";
48 |
49 | // Should fail when called by non-owner
50 | vm.expectRevert();
51 | agreement.setProtocolName(newName);
52 |
53 | // Should succeed when called by owner
54 | vm.prank(owner);
55 | vm.expectEmit();
56 | emit V2.AgreementV2.AgreementUpdated();
57 | agreement.setProtocolName(newName);
58 |
59 | V2.AgreementDetailsV2 memory _details = agreement.getDetails();
60 | assertEq(_details.protocolName, newName);
61 | }
62 |
63 | function testSetContactDetails() public {
64 | V2.Contact[] memory newContacts = new V2.Contact[](2);
65 | newContacts[0] = V2.Contact({name: "New Contact 1", contact: "@newcontact1"});
66 |
67 | // Should fail when called by non-owner
68 | vm.expectRevert();
69 | agreement.setContactDetails(newContacts);
70 |
71 | // Should succeed when called by owner
72 | vm.prank(owner);
73 | vm.expectEmit();
74 | emit V2.AgreementV2.AgreementUpdated();
75 | agreement.setContactDetails(newContacts);
76 |
77 | V2.AgreementDetailsV2 memory _details = agreement.getDetails();
78 | assertEq(keccak256(abi.encode(newContacts)), keccak256(abi.encode(_details.contactDetails)));
79 | }
80 |
81 | function testAddChains() public {
82 | V2.Account[] memory accounts = new V2.Account[](1);
83 | accounts[0] = V2.Account({accountAddress: "0x04", childContractScope: V2.ChildContractScope.None});
84 |
85 | V2.Chain[] memory newChains = new V2.Chain[](1);
86 | newChains[0] = V2.Chain({assetRecoveryAddress: "0x05", accounts: accounts, caip2ChainId: "eip155:2"});
87 |
88 | // Should fail when called by non-owner
89 | vm.expectRevert();
90 | agreement.addChains(newChains);
91 |
92 | // Should fail when the chain is invalid
93 | V2.Chain[] memory invalidChains = new V2.Chain[](1);
94 | invalidChains[0] = V2.Chain({assetRecoveryAddress: "0x06", accounts: accounts, caip2ChainId: "eip155:999"});
95 |
96 | vm.prank(owner);
97 | vm.expectRevert(abi.encodeWithSelector(V2.AgreementV2.InvalidChainId.selector, "eip155:999"));
98 | agreement.addChains(invalidChains);
99 |
100 | // Should succeed when called by owner
101 | vm.prank(owner);
102 | vm.expectEmit();
103 | emit V2.AgreementV2.AgreementUpdated();
104 | agreement.addChains(newChains);
105 |
106 | V2.AgreementDetailsV2 memory _details = agreement.getDetails();
107 | V2.Chain memory _chain = _details.chains[_details.chains.length - 1];
108 | assertEq(keccak256(abi.encode(newChains[0])), keccak256(abi.encode(_chain)));
109 |
110 | // Should fail when adding duplicate chain
111 | vm.prank(owner);
112 | vm.expectRevert();
113 | agreement.addChains(newChains);
114 | }
115 |
116 | function testSetChains() public {
117 | V2.Account[] memory accounts = new V2.Account[](1);
118 | accounts[0] = V2.Account({accountAddress: "0x04", childContractScope: V2.ChildContractScope.None});
119 |
120 | V2.Chain[] memory chains = new V2.Chain[](1);
121 | chains[0] = V2.Chain({
122 | assetRecoveryAddress: "0x05",
123 | accounts: accounts,
124 | caip2ChainId: "eip155:1" // Update existing chain
125 | });
126 |
127 | // Should fail when called by non-owner
128 | vm.expectRevert();
129 | agreement.setChains(chains);
130 |
131 | // Should fail when chain doesn't exist
132 | V2.Chain[] memory nonExistentChains = new V2.Chain[](1);
133 | nonExistentChains[0] = V2.Chain({
134 | assetRecoveryAddress: "0x05",
135 | accounts: accounts,
136 | caip2ChainId: "eip155:999" // Non-existent chain
137 | });
138 |
139 | vm.prank(owner);
140 | vm.expectRevert();
141 | agreement.setChains(nonExistentChains);
142 |
143 | // Should succeed when called by owner
144 | vm.prank(owner);
145 | vm.expectEmit();
146 | emit V2.AgreementV2.AgreementUpdated();
147 | agreement.setChains(chains);
148 |
149 | V2.AgreementDetailsV2 memory _details = agreement.getDetails();
150 | assertEq(_details.chains.length, 1);
151 | assertEq(keccak256(abi.encode(chains[0])), keccak256(abi.encode(_details.chains[0])));
152 | }
153 |
154 | function testRemoveChain() public {
155 | V2.Account[] memory accounts = new V2.Account[](1);
156 | accounts[0] = V2.Account({accountAddress: "0x01", childContractScope: V2.ChildContractScope.None});
157 |
158 | V2.Chain[] memory newChains = new V2.Chain[](1);
159 | newChains[0] = V2.Chain({assetRecoveryAddress: "0x05", accounts: accounts, caip2ChainId: "eip155:2"});
160 |
161 | vm.prank(owner);
162 | agreement.addChains(newChains);
163 |
164 | // Should fail when called by non-owner
165 | vm.expectRevert();
166 | string[] memory chainToRemove = new string[](1);
167 | chainToRemove[0] = "eip155:2";
168 | agreement.removeChains(chainToRemove);
169 |
170 | // Should fail when removing non-existent chain
171 | vm.prank(owner);
172 | vm.expectRevert();
173 | string[] memory nonExistentChain = new string[](1);
174 | nonExistentChain[0] = "eip155:999";
175 | agreement.removeChains(nonExistentChain);
176 |
177 | // Should succeed when called by owner
178 | vm.prank(owner);
179 | vm.expectEmit();
180 | emit V2.AgreementV2.AgreementUpdated();
181 | agreement.removeChains(chainToRemove);
182 |
183 | // Verify the change
184 | V2.AgreementDetailsV2 memory _details = agreement.getDetails();
185 | assertEq(keccak256(abi.encode(_details)), keccak256(abi.encode(details)));
186 | }
187 |
188 | // Test adding accounts to a chain
189 | function testAddAccounts() public {
190 | V2.Account[] memory accounts = new V2.Account[](1);
191 | accounts[0] = V2.Account({accountAddress: "0x01", childContractScope: V2.ChildContractScope.None});
192 |
193 | // Should fail when called by non-owner
194 | vm.expectRevert();
195 | agreement.addAccounts("eip155:1", accounts);
196 |
197 | // Should fail when adding to non-existent chain
198 | vm.prank(owner);
199 | vm.expectRevert();
200 | agreement.addAccounts("eip155:999", accounts);
201 |
202 | // Should succeed when called by owner
203 | vm.prank(owner);
204 | vm.expectEmit();
205 | emit V2.AgreementV2.AgreementUpdated();
206 | agreement.addAccounts("eip155:1", accounts);
207 |
208 | // Verify the change
209 | V2.AgreementDetailsV2 memory _details = agreement.getDetails();
210 | V2.Account memory _account = _details.chains[0].accounts[_details.chains[0].accounts.length - 1];
211 |
212 | assertEq(keccak256(abi.encode(accounts[0])), keccak256(abi.encode(_account)));
213 | }
214 |
215 | function testRemoveAccount() public {
216 | V2.Account[] memory accounts = new V2.Account[](1);
217 | accounts[0] = V2.Account({accountAddress: "0x02", childContractScope: V2.ChildContractScope.None});
218 |
219 | vm.prank(owner);
220 | agreement.addAccounts("eip155:1", accounts);
221 |
222 | // Should fail when called by non-owner
223 | vm.expectRevert();
224 | string[] memory accountToRemove = new string[](1);
225 | accountToRemove[0] = "0x02";
226 | agreement.removeAccounts("eip155:1", accountToRemove);
227 |
228 | // Should fail when removing from non-existent chain
229 | vm.prank(owner);
230 | vm.expectRevert();
231 | agreement.removeAccounts("eip155:999", accountToRemove);
232 |
233 | // Should fail when removing non-existent account
234 | vm.prank(owner);
235 | vm.expectRevert();
236 | string[] memory nonExistentAccount = new string[](1);
237 | nonExistentAccount[0] = "0x999";
238 | agreement.removeAccounts("eip155:1", nonExistentAccount);
239 |
240 | // Should succeed when called by owner
241 | vm.prank(owner);
242 | vm.expectEmit();
243 | emit V2.AgreementV2.AgreementUpdated();
244 | agreement.removeAccounts("eip155:1", accountToRemove);
245 |
246 | // Verify the change - should be back to original state
247 | V2.AgreementDetailsV2 memory _details = agreement.getDetails();
248 | assertEq(keccak256(abi.encode(_details)), keccak256(abi.encode(details)));
249 | }
250 |
251 | // Test setting bounty terms
252 | function testSetBountyTerms() public {
253 | V2.BountyTerms memory newTerms = details.bountyTerms;
254 | newTerms.bountyPercentage = 20;
255 | newTerms.bountyCapUSD = 2000000;
256 |
257 | // Should fail when called by non-owner
258 | vm.expectRevert();
259 | agreement.setBountyTerms(newTerms);
260 |
261 | // Should succeed when called by owner
262 | vm.prank(owner);
263 | vm.expectEmit();
264 | emit V2.AgreementV2.AgreementUpdated();
265 | agreement.setBountyTerms(newTerms);
266 |
267 | // Verify the change
268 | V2.AgreementDetailsV2 memory _details = agreement.getDetails();
269 | assertEq(keccak256(abi.encode(newTerms)), keccak256(abi.encode(_details.bountyTerms)));
270 |
271 | // Should fail when trying to set both aggregateBountyCapUSD and retainable
272 | newTerms.aggregateBountyCapUSD = 1000000;
273 | newTerms.retainable = true;
274 | vm.prank(owner);
275 | vm.expectRevert(V2.AgreementV2.CannotSetBothAggregateBountyCapUSDAndRetainable.selector);
276 | agreement.setBountyTerms(newTerms);
277 | }
278 |
279 | function testConstructorCannotSetBothAggregateBountyCapUSDAndRetainable() public {
280 | V2.AgreementDetailsV2 memory invalidDetails = getMockAgreementDetails("0x01");
281 | invalidDetails.bountyTerms.aggregateBountyCapUSD = 1000;
282 | invalidDetails.bountyTerms.retainable = true;
283 |
284 | // Should fail when both conditions are true in constructor
285 | vm.expectRevert(V2.AgreementV2.CannotSetBothAggregateBountyCapUSDAndRetainable.selector);
286 | new V2.AgreementV2(invalidDetails, address(registry), owner);
287 | }
288 |
289 | function testConstructorDuplicateChainValidation() public {
290 | V2.Account[] memory accounts = new V2.Account[](1);
291 | accounts[0] = V2.Account({accountAddress: "0x01", childContractScope: V2.ChildContractScope.All});
292 |
293 | V2.Chain memory chain = V2.Chain({accounts: accounts, assetRecoveryAddress: "0x01", caip2ChainId: "eip155:1"});
294 |
295 | V2.Chain[] memory duplicateChains = new V2.Chain[](2);
296 | duplicateChains[0] = chain;
297 | duplicateChains[1] = chain;
298 |
299 | V2.AgreementDetailsV2 memory invalidDetails = V2.AgreementDetailsV2({
300 | protocolName: "testProtocol",
301 | chains: duplicateChains,
302 | contactDetails: details.contactDetails,
303 | bountyTerms: details.bountyTerms,
304 | agreementURI: "ipfs://testHash"
305 | });
306 |
307 | vm.expectRevert(abi.encodeWithSelector(V2.AgreementV2.DuplicateChainId.selector, "eip155:1"));
308 | new V2.AgreementV2(invalidDetails, address(registry), owner);
309 | }
310 |
311 | function testConstructorInvalidChainValidation() public {
312 | V2.Account[] memory accounts = new V2.Account[](1);
313 | accounts[0] = V2.Account({accountAddress: "0x01", childContractScope: V2.ChildContractScope.All});
314 |
315 | V2.Chain memory chain = V2.Chain({accounts: accounts, assetRecoveryAddress: "0x01", caip2ChainId: "eip155:999"});
316 |
317 | V2.Chain[] memory invalidChains = new V2.Chain[](2);
318 | invalidChains[0] = chain;
319 |
320 | V2.AgreementDetailsV2 memory invalidDetails = V2.AgreementDetailsV2({
321 | protocolName: "testProtocol",
322 | chains: invalidChains,
323 | contactDetails: details.contactDetails,
324 | bountyTerms: details.bountyTerms,
325 | agreementURI: "ipfs://testHash"
326 | });
327 |
328 | vm.expectRevert(abi.encodeWithSelector(V2.AgreementV2.InvalidChainId.selector, "eip155:999"));
329 | new V2.AgreementV2(invalidDetails, address(registry), owner);
330 | }
331 | }
332 |
--------------------------------------------------------------------------------
/documents/summary.tex:
--------------------------------------------------------------------------------
1 | \documentclass{article}
2 | \usepackage{import}
3 | \usepackage{titling}
4 | \usepackage{enumitem}
5 | \usepackage[margin=1in]{geometry}
6 | \usepackage{hyperref}
7 | \hypersetup{
8 | colorlinks=true,
9 | linkcolor=blue,
10 | filecolor=blue,
11 | urlcolor=blue,
12 | pdfpagemode=FullScreen,
13 | }
14 | \usepackage[none]{hyphenat}
15 | \tolerance=10000
16 | \emergencystretch=3em
17 | \newcommand{\fullref}[1]{%
18 | Section~\ref{sec:definitions}, \nameref{subsec:core}, item~\ref{#1}%
19 | }
20 |
21 | \title{\large
22 | TECHNICAL SUMMARY}
23 | \author{}
24 | \date{}
25 |
26 | \begin{document}
27 |
28 | \maketitle
29 |
30 | \section*{}
31 | \textbf{PLEASE READ ALL OF THE WHITEHAT SAFE HARBOR AGREEMENT (THE “AGREEMENT”) VERY CAREFULLY. THE BULLET POINTS BELOW ARE ONLY A PARTIAL SUMMARY OF SOME OF THE MATERIAL TERMS OF THE AGREEMENT. IN ALL CIRCUMSTANCES, INCLUDING IN THE EVENT OF ANY CONFLICT OR INCONSISTENCY BETWEEN THIS OR ANY OTHER SUMMARY AND THE TEXT OF THE AGREEMENT, THE TEXT OF THE AGREEMENT WILL GOVERN, TO THE EXCLUSION OF THE SUMMARY. CERTAIN TERMS USED IN THIS SUMMARY ARE DEFINED IN THE AGREEMENT. BE RESPONSIBLE AND THOROUGH - FAILING TO FOLLOW THE TERMS OF THE AGREEMENT COULD RESULT IN SERIOUS LEGAL CONSEQUENCES.}
32 |
33 | \section{MOTIVATION AND BACKGROUND}
34 | \begin{itemize}
35 | \item The intention of the agreement is to provide a framework that:
36 | \begin{itemize}
37 | \item Rewards whitehats for locating active exploits,
38 | \item Allows whitehats to proactively secure protocol funds, and
39 | \item Protects \textit{responsible} actors against legal risk.
40 | \end{itemize}
41 | \item As a “whitehat,” you should act competently and in good faith.
42 | \begin{itemize}
43 | \item While there is no formal standard of “competence,” a competent whitehat has some background experience in software engineering, security, and/or blockchain auditing.
44 | \item Hacking a protocol affects other people's money and can have irreversible consequences. Proceed with caution, act ethically, and execute well.
45 | \end{itemize}
46 | \item \textbf{Provided that you do act lawfully, competently and in good faith, the protocol and its members waive the right to pursue legal claims against you.} However, be aware that the legal landscape is complex, and engaging in agreements of this nature carries associated risks. Exercise caution and seek advice as necessary.
47 | \item If you successfully rescue and return exploited assets to the Asset Recovery Address (ARA) in compliance with the agreement, you may be entitled to a reward (typically proportional to your transfers to the ARA).
48 | \end{itemize}
49 |
50 | \section{CHECKLIST}
51 |
52 | \begin{itemize}
53 | \item Is this an active, urgent exploit?
54 | \item Are you unable to responsibly disclose the exploit (e.g. via a bug bounty program) due to time constraints or other reasons?
55 | \item Can you reasonably expect your intervention to be net beneficial, reducing total losses to the protocol and associated entities?
56 | \item Are you experienced and confident in your ability to manage execution risk, avoiding unintentional loss of funds?
57 | \item Will you avoid intentionally profiting from the exploit in any way other than through the reward granted by the protocol?
58 | \item Are you and anyone with whom you directly cooperate during the funds rescue, as well as all funds and addresses used in said rescue, free from OFAC sanctions and/or other connections to sanctioned parties?
59 | \item Have you confirmed the agreement has been duly adopted by the protocol community?
60 | \item Are you fully aware of the risks associated with your actions, including but not limited to accidental loss of funds, claims and liabilities outside this agreement's scope, and the unclear extent of this agreement's enforceability?
61 | \item Have you thoroughly read the entire agreement and understand all of its terms and conditions?
62 | \end{itemize}
63 |
64 | Before executing a funds rescue and/or depositing funds to an ARA, always confirm that the conditions listed above still hold.
65 |
66 | \section{THE AGREEMENT}
67 |
68 | \begin{itemize}
69 | \item \textit{Main point:} If you follow this agreement and meet its requirements as a whitehat, the protocol and its users agree not to take legal action against you or raise complaints to the government in connection with your actions under the agreement.
70 | \begin{itemize}
71 | \item The aim of this agreement is to enable rewards for whitehats and provide legal protection for proactively securing funds against active exploits. By adopting it, the protocol gives you the freedom to act in its best interest, in situations where following an ordinary bug-bounty disclosure program may be impossible or impractical.
72 | \item This agreement covers the protocol and its users, but does not (and cannot) cover the actions of government or regulatory entities. You should still proceed with utmost caution.
73 | \item The protocol can change the terms of the agreement at any time prior to an exploit, including what is in or out of scope. It is your responsibility to be aware of the most current version.
74 | \end{itemize}
75 |
76 | \item \textit{Exploits}: You may be entitled to a reward for performing a \textbf{funds rescue}, which must meet the following conditions.
77 | \begin{itemize}
78 | \item Deposits all tokens removed from the protocol into the ARA, possibly excluding any \textbf{retained reward} allowed under the agreement.
79 | \item Addresses an active threat that has already been triggered by someone else. Only \textit{active exploits} are covered \- you are not allowed to start the process, but you can finish it.
80 | \item Follows the specified process in the agreement, including any addenda.
81 | \item Notifies the protocol as soon as reasonably practicable, such as immediately after the funds rescue is complete. If for any reason you cannot deposit funds to the ARA within 6 hours post-rescue, you must notify the protocol.
82 | \item Is performed by whitehats who can make the necessary representations and warranties. If you cooperate with anyone, pick known good actors.
83 | \item Note that by default, you are considered a \textbf{prospective whitehat} who makes a conscious decision to initiate a rescue. However, if an automated contract (or \textbf{generalized arbitrage bot}) owned or operated by you has already performed an exploit, you are instead considered a \textbf{retrospective whitehat}, in which case you must notify the protocol and initiate the return of funds once you become aware of the exploit.
84 | \end{itemize}
85 |
86 | \item \textit{Expenses and rewards}
87 | \begin{itemize}
88 | \item You are allowed to incur reasonable expenses in the course of the rescue (e.g. gas fees and slippage costs).
89 | \item You should attempt to minimize unnecessary expenses. Don't destroy the value of the assets while saving them.
90 | \item Any proportional reward is calculated based on the US dollar value of the \textbf{returnable assets}, equal to the exploited assets minus any funds used in good faith to rescue and deposit those assets. SEAL recommends a 10\% Bounty, but the Bounty percentage may be adjusted or capped in a specific protocol agreement.
91 | \item Protocols may also specify an \texttt{aggregateBountyCapUSD}, which limits the total bounty payouts across all whitehats for a single exploit. If this cap is exceeded, rewards are reduced proportionally.
92 | \item Your reward is based on the funds you individually secured, and will be transferred by default to the originating address used during the rescue.
93 | \end{itemize}
94 |
95 | \item \textit{Receiving rewards}: You may use either of the following two methods.
96 | \begin{itemize}
97 | \item Return all assets to the ARA and specify, through a clearly identifiable public message (e.g. an event or transaction payload), where you wish to receive the reward.
98 | \item Return all assets to the ARA \textit{except} the designated reward. Deposit the reward in an address that you verify in writing to the protocol publicly, as with method (A).
99 | \end{itemize}
100 | In either case, the protocol has 15 days to initiate a dispute. You may presume the reward is accepted unless you are notified otherwise.
101 |
102 | \item If the protocol decides you've broken the agreement, it can refuse to pay your reward even if you've already completed a funds rescue. If you completed a valid rescue, you may be able to still claim a reward through the dispute resolution process provided in the agreement. For more details, see \textbf{dispute resolution}.
103 | \end{itemize}
104 |
105 | \section{COVENANTS YOU ARE AGREEING TO AS A WHITEHAT}
106 |
107 | \begin{itemize}
108 | \item You have read and understood the full agreement (\textbf{not just this summary}), including any modifications made by the protocol when adopting the agreement.
109 | \item All secondary actions taken in connection with the funds rescue are legal.
110 | \item You will follow all necessary precautions to prevent collateral damage. For instance, you will not execute transactions via a public mempool vulnerable to frontrunning or similar forms of interference.
111 | \item The protocol is not responsible for monitoring you or ensuring you follow the law.
112 | \item Participating does not make you an employee or representative of the protocol, nor does it create any exclusive relationship.
113 | \item The reward outlined in the agreement is the only compensation due.
114 | \item The protections outlined in the agreement apply only if the protocol agrees that you have not violated its terms.
115 | \item Provided you follow the agreement, neither the protocol nor its members may pursue present or future legal claims against you in connection to the funds rescue.
116 | \item However, you also waive any claims against the protocol and its members.
117 | \end{itemize}
118 |
119 | \section{REPRESENTATIONS AND WARRANTIES YOU MAKE AS A WHITEHAT}
120 |
121 | \begin{itemize}
122 | \item You are legally able to enter into this agreement.
123 | \item Any blockchain addresses and any additional funds used to perform the rescue are clean, and not obtained illegally or from a sanctioned source.
124 | \item You are not currently subject to sanctions from OFAC.
125 | \item You are not a senior political official.
126 | \item You are not violating any other agreement by participating in this one.
127 | \item You have sufficient experience in blockchain security to perform the rescue competently, and have weighed the risks and benefits of doing so.
128 | \item You are not currently the target of legal action related to other blockchain exploits.
129 | \item You either own or have a valid license for any tools and intellectual property used in the course of the rescue.
130 | \item You have not triggered the blackhat exploit yourself (which would nullify the agreement), for instance by posing as a third party.
131 | \end{itemize}
132 |
133 | \section{INDEMNIFICATION AND DISPUTE RESOLUTION}
134 |
135 | \begin{itemize}
136 | \item If you break this agreement, you may have to reimburse affected protocol members and may be subject to criminal prosecution.
137 | \item Either party may initiate a dispute for issues not resolved after 30 days. Disputes are resolved via binding arbitration, which will take place in Singapore under the administration of SIAC (Singapore International Arbitration Centre) unless otherwise specified by the agreement.
138 | \item If a dispute does arise, each side must pay half of the initial fees for the arbitrator and half of the regular expenses during the proceedings. All other costs, including attorney fees, will be paid by the loser after a judgment is reached.
139 | \item No member of the protocol can press claims of their own without the protocol's approval.
140 | \end{itemize}
141 |
142 | \section{COMPLIANCE WITH LAWS}
143 |
144 | \begin{itemize}
145 | \item The protocol is not responsible for ensuring that, aside from the rescue itself, you are following the law both generally and with respect to the protocol.
146 | \item The protocol and its users will neither pursue nor assist any claims against you in connection with the rescue.
147 | \end{itemize}
148 |
149 | \section{MISCELLANEOUS PROVISIONS}
150 |
151 | \begin{itemize}
152 | \item You can communicate with the protocol via the email listed in the agreement.
153 | \item The protocol can communicate with you via any address you use to make deposits to the ARA.
154 | \item You are responsible for any taxable events that occur as a result of the rescue. Generally, the safest and best thing to do is deposit funds \textit{directly} to the ARA so that they never enter your direct possession.
155 | \item By participating, you waive your right to any class-action suits or trial by jury (because disputes are covered by arbitration).
156 | \end{itemize}
157 |
158 | \section{EXHIBITS AND ADDENDA}
159 |
160 | \begin{itemize}
161 | \item Exhibit A specifies certain defined terms used in the Agreement.
162 | \item Exhibit B outlines the recommended format for proposing adoption of the Agreement to a DAO.
163 | \item Exhibit C provides a form of consent for the Security Team to sign to adopt the Agreement.
164 | \item Exhibit D provides a form of procedures to be included in web applications and other user interfaces facilitating use of the Protocol to bind Users to the Agreement.
165 | \item Exhibit E provides recommended risk disclosures relating to adoption and use of the Agreement.
166 | \item Exhibit F provides an FAQ relating to the Agreement.
167 | \end{itemize}
168 |
169 | Things you must always check:
170 |
171 | \begin{itemize}
172 | \item List of eligible and ineligible exploits
173 | \item The Asset Recovery Address matches on all relevant communications and the on-chain registry
174 | \item There is not already a bug bounty program and responsible disclosure process in place that you can and should execute first
175 | \item You have NO OTHER PROFIT MOTIVE other than that of the reward. Anyone found to have an extraneous motive would not qualify for immunity
176 | \end{itemize}
177 |
178 |
179 | Full summary including procedural recommendations found here: \url{https://docs.google.com/document/d/1sTpU37r8JPEAsxG3Y-Rf0pWMOEumTc2_QijZbSpSRW0/edit#heading=h.7z81worfyiy}
180 |
181 | \vspace{5mm}
182 |
183 | BY PARTICIPATING IN THE PROGRAM, YOU WILL BE ENTERING INTO AND CONSENTING TO BE BOUND BY, AND ASSENTING TO THE TERMS AND CONDITIONS SET FORTH IN THE AGREEMENT, AND WILL BE DEEMED A PARTY TO THE AGREEMENT. PARTICIPATING IN THE PROGRAM INCLUDES, WITHOUT LIMITATION, TAKING ANY ACTION PURSUANT TO, OR SEEKING TO RECEIVE ANY OF THE RIGHTS OR BENEFITS RELATED TO, THE PROGRAM, AS SET FORTH IN THE AGREEMENT.
184 |
185 | \vspace{5mm}
186 |
187 | NOTICE TO PARTICIPATING WHITEHAT, IF YOU DO NOT ABIDE BY, AND PERFORM ALL OF THE TERMS AND CONDITIONS OF THE AGREEMENT, OR IF ANY OF THE REPRESENTATIONS AND WARRANTIES SET FORTH IN THE AGREEMENT ARE INACCURATE AS APPLIED TO YOU, YOU MAY FAIL TO BE ELIGIBLE FOR OR ENTITLED TO ANY OR ALL RIGHTS OR BENEFITS UNDER THE AGREEMENT, INCLUDING ANY RIGHT TO RECEIVE OR RETAIN ANY BOUNTIES.
188 |
189 | \vspace{5mm}
190 |
191 | Please contact us at safeharbor@securityalliance.org for any questions or issues.
192 |
193 | \end{document}
--------------------------------------------------------------------------------