{"id":18263318,"url":"https://github.com/safe-global/safe-locking","last_synced_at":"2025-10-25T12:45:34.629Z","repository":{"id":235457854,"uuid":"745415177","full_name":"safe-global/safe-locking","owner":"safe-global","description":"Safe Token Locking Contracts","archived":false,"fork":false,"pushed_at":"2024-05-04T07:38:40.000Z","size":974,"stargazers_count":2,"open_issues_count":0,"forks_count":2,"subscribers_count":7,"default_branch":"main","last_synced_at":"2025-03-29T07:33:46.071Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"lgpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/safe-global.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-01-19T09:36:30.000Z","updated_at":"2024-05-04T07:38:43.000Z","dependencies_parsed_at":"2024-11-05T11:14:21.633Z","dependency_job_id":"0fd743f9-2dd1-4396-ab4c-6c553a627ac9","html_url":"https://github.com/safe-global/safe-locking","commit_stats":null,"previous_names":["safe-global/safe-locking"],"tags_count":1,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/safe-global%2Fsafe-locking","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/safe-global%2Fsafe-locking/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/safe-global%2Fsafe-locking/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/safe-global%2Fsafe-locking/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/safe-global","download_url":"https://codeload.github.com/safe-global/safe-locking/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":249689072,"owners_count":21311306,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-05T11:10:45.847Z","updated_at":"2025-10-25T12:45:29.584Z","avatar_url":"https://github.com/safe-global.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Safe Token Locking Contract\n\n## Introduction\n\nSafe locking contract facilitates locking Safe tokens. Users can lock and unlock tokens anytime and also withdraw after the `COOLDOWN_PERIOD` is over. The contract also provides feature controlled by the admin address to recover ERC20 tokens other than Safe tokens.\n\n## Mainnet Deployment\n\nThe Safe locking contract was deployed to Ethereum mainnet at address [`0x0a7CB434f96f65972D46A5c1A64a9654dC9959b2`](https://etherscan.io/address/0x0a7CB434f96f65972D46A5c1A64a9654dC9959b2). The deployment transaction was [`0xd90bbe97f71ca9cff6e7ee545a773d25fa34260624df681bf04592f5e7301b2e`](https://etherscan.io/tx/0xd90bbe97f71ca9cff6e7ee545a773d25fa34260624df681bf04592f5e7301b2e).\n\n## Contract\n\n### Contract behaviour\n\n- The locking contract is designed for \"continuous\" locking (as opposed to a “discrete” locking mechanism). This means that:\n  - Tokens can be locked at any time\n  - Tokens can be unlocked at any time, where a transaction is issued to initiate the unlock and enter a “cool-down” period and a follow-up transaction is issued to withdraw the unlocked tokens after the cool-down period is over.\n- The current specification considers locking age to be computed in a First-In-First-Out order (Calculated offchain). For example, a user:\n\n  1.  Locks **100 SAFE** on **block** **1000000**\n  2.  Locks **50 SAFE** on **block** **1000500**\n  3.  Unlocks, then withdraws **75 SAFE**\n\n  On block **1001000**, their locked SAFE amounts with ages would be:\n\n  - **25 SAFE** locked for **1000 blocks**\n  - **50 SAFE** locked for **500 blocks**\n\n- `withdraw(...)` is defined to withdraw all matured unlocks (unlocks whose unlock time is greater than `block.timestamp`). This is an `O(n)` operation (where `n` is the number of matured unlocks). In order to support more deterministic gas usage, a `withdraw(maxUnlocks)` function is also provided to withdraw up to `maxUnlocks` of the oldest matured unlocks. For example, `withdraw(1)` can be used to withdraw only the oldest unlock if it is already matured, which is an `O(1)` operation.\n- `getUserTokenBalance` returns the total `SAFE` token balance belonging to the `holder` within the locking contract, this includes locked tokens, unlocked tokens, and ready to withdraw tokens.\n- Full accounting of locked tokens per user can be computable based on emitted events.\n- The total `SAFE` token balance in the locking contract belonging to any user can be readable on-chain or via a standard `eth_call` JSON RPC request. This MUST be made available by the `getUserTokenBalance(...)` contract method.\n- The locking contract can allow simultaneous token unlock requests with separate cooldown ending timestamps based on an immutable cooldown period.\n- The contract cannot allow the total sum of unlock request amounts to exceed the token holder’s `SAFE` balance in the contract.\n- A withdrawal will withdraw the complete amount of its corresponding unlock request. That is, if a user calls `unlock(amount)`, then `withdraw()`, that unlock once matured, should transfer exactly `amount` `SAFE` tokens to the holder (assuming no other unlocks were matured apart from that particular unlock operation).\n\n#### Possible states for Tokens\n\n- `Locked`: When the tokens are transferred from User using `transferFrom` for an indefinite period.\n- `Unlocked`: Inititated by a user who already have certain amount of tokens locked. This state merely starts the unlock process from the current timestamp to a cooldown timestamp (calculated based on cooldown period).\n- `Withdrawable`: This is the state of unlocked tokens ready to be withdrawn completely by the user. The tokens can remain in the contract indefinitely in this state, until the user withdraws.\n- `Withdrawn`: This state is atomic, and can only be tracked on-chain based on logs emitted during the withdraw operation.\n\n**Note**: Token enters the contract in `Locked` state (unless tokens are transferred directly without calling the `lock(...)`) and exits the contract from a `Withdrawable` state using the `withdraw(...)` to `Withdrawn` state as mentioned above.\n\n### Contract State Change Explanation\n\n| User | Operation | Time | Amount | users[User]                             | unlocks[index][User]                                | Note                                         |\n| ---- | --------- | ---- | ------ | --------------------------------------- | --------------------------------------------------- | -------------------------------------------- |\n| A    | Lock      |      | X1     | (X1, 0, 0, 0)                           |                                                     |                                              |\n| A    | Unlock    |      | X2     | (X1 - X2, X2, 0, 1)                     | unlocks[0][A] → (X2, t1)                            |                                              |\n| B    | Lock      |      | Y1     | (Y1, 0, 0, 0)                           |                                                     |                                              |\n| A    | Unlock    |      | X3     | (X1 - X2 - X3, X2 + X3, 0, 2)           | unlocks[1][A] → (X3, t2)                            |                                              |\n| B    | Unlock    |      | Y2     | (Y1 - Y2, Y2, 0, 1)                     | unlocks[0][B] → (Y2, t2)                            |                                              |\n| A    | Unlock    |      | X4     | (X1 - X2 - X3 - X4, X2 + X3 + X4, 0, 3) | unlocks[2][A] → (X4, t2)                            |                                              |\n| A    | Withdraw  | t1   |        | (X1 - X2 - X3 - X4, X3 + X4, 1, 3)      | unlocks[0][A] → (0, 0) ???                          |                                              |\n| B    | Unlock    |      | Y3     | (Y1 - Y2 - Y3, Y2 + Y3, 0, 2)           | unlocks[1][B] → (Y3, t3)                            |                                              |\n| A    | Withdraw  | t2   |        | (X1 - X2 - X3 - X4, 0, 3, 3)            | unlocks[2][A] → (0, 0) \u0026 unlocks[3][A] → (0, 0) ??? | Here 2 withdraw happens, as time t2 reached. |\n| B    | Withdraw  |      |        | (Y1 - Y2 - Y3, Y3, 1, 2)                | unlocks[0][B] → (0, 0) ???                          |                                              |\n\nBasic assumptions like `X1 \u003e X2 + X3 + X4` and `Y1 \u003e Y2 + Y3` are applicable. For a numerical example, please check [here](./docs/Implementation.md).\n\n### Contract Implementation Details\n\nContract implementation details can be found in the [docs folder](./docs/Implementation.md).\n\n**Note**: Any Safe tokens sent to the contract other than use of `lock(...)` function cannot be recovered and will be deposited to the contract forever.\n\n### User Actions Sequence Diagram\n\n```mermaid\nsequenceDiagram\n\tactor H as Holder\n\tparticipant T as SAFE Token\n\tparticipant L as Locking Contract\n\n\tnote over H,L: Locking\n\tH  -\u003e\u003e  T: approve(lockingContract, a)\n\tH  -\u003e\u003e+ L: lock(a)\n\tL  -\u003e\u003e+ T: transferFrom(msg.sender, this, a)\n\tT --\u003e\u003e- L: a SAFE\n\tL  -\u003e\u003e  L: emit Locked()\n\tdeactivate L\n\n\tnote over H,L: Unlocking\n\tH  -\u003e\u003e+ L: unlock(b)\n\tL  -\u003e\u003e  L: emit Unlocked()\n\tL --\u003e\u003e- H: id\n\n\tnote over H,L: Withdrawal\n\tH  -\u003e\u003e  H: ... wait for cooldown period ...\n\tH  -\u003e\u003e+ L: withdraw()\n\tL  -\u003e\u003e+ T: transfer(msg.sender, b)\n\tT --\u003e\u003e- H: b SAFE\n\tL  -\u003e\u003e  L: emit Withdrawn()\n\tdeactivate L\n```\n\n## Audits\n\n- [Ackee Audit Report](./docs/audits/ackee-blockchain-safe-token-locking-report-1.1.pdf)\n- [Certora Audit Report](./docs/audits/Safe%20-%20Certora%20security%20report.pdf)\n\n## Usage\n\n### Install requirements with yarn:\n\n```bash\nnpm ci\n```\n\n### Run tests:\n\n#### Locally\n\n```bash\nnpm run test\n```\n\n#### E2E\n\n```bash\nnpm run test:e2e\n```\n\nNOTE: To run E2E test, `INFURA_KEY` in `.env` with the appropriate value to fork the Ethereum Mainnet.\n\n#### All tests\n\n```bash\nnpm run test:all\n```\n\n#### Formal Verification\n\nFirst setup a Python virtual environment and install required Certora dependencies:\n\n```bash\npython3 -m venv venv\n. venv/bin/activate\npip install -r certora/requirements.txt\n```\n\nInstall the correct `solc` binary version. On Linux:\n\n```bash\nwget https://github.com/ethereum/solidity/releases/download/v0.8.23/solc-static-linux\nchmod +x solc-static-linux\nsudo mv solc-static-linux /some/directory/in/your/path/solc-0.8.23\n```\n\nTo execute the formal verification specification:\n\n```bash\nexport CERTORAKEY=\"...\"\ncertoraRun certora/conf/SafeTokenLock.conf\n```\n\nNote: To use a Solidity compiler at custom path, run with:\n\n```bash\ncertoraRun certora/conf/SafeTokenLock.conf --solc /path/to/solc\n```\n\n### Deploy\n\n\u003e :warning: **Make sure to use the correct commit when deploying the contracts.** Any change (even comments) within the contract files will result in different addresses. The tagged versions can be found in the [releases](https://github.com/safe-global/safe-locking/releases).\n\nThis will deploy the contracts deterministically and verify the contracts on etherscan using [Solidity 0.8.23](https://github.com/ethereum/solidity/releases/tag/v0.8.23) by default.\n\nPreparation:\n\n- Set `MNEMONIC` in `.env`\n- Set `INFURA_KEY` in `.env`\n\n```bash\nnpm run deploy-all \u003cnetwork\u003e\n```\n\nThis will perform the following steps\n\n```bash\nnpm run build\nnpm run hardhat --network \u003cnetwork\u003e deploy\nnpm run hardhat --network \u003cnetwork\u003e etherscan-verify\nnpm run hardhat --network \u003cnetwork\u003e local-verify\n```\n\n#### Custom Networks\n\nIt is possible to use the `CUSTOM_NODE_URL` env var to connect to any EVM based network via an RPC endpoint. This connection then can be used with the `custom` network.\n\nE.g. to deploy the Safe contract suite on that network you would run `npm run deploy-all custom`.\n\nThe resulting addresses should be on all networks the same.\n\nNote: Address will vary if contract code is changed or a different Solidity version is used.\n\n## Security and Liability\n\nAll contracts are WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n\n## License\n\nAll smart contracts are released under LGPL-3.0\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsafe-global%2Fsafe-locking","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsafe-global%2Fsafe-locking","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsafe-global%2Fsafe-locking/lists"}