Udsen
    • Create new note
    • Create a note from template
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Write
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Engagement control Commenting, Suggest edit, Emoji Reply
      • Invitee
    • Publish Note

      Publish Note

      Everyone on the web can find and read all notes of this public team.
      Once published, notes can be searched and viewed by anyone online.
      See published notes
      Please check the box to agree to the Community Guidelines.
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Suggest edit
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Engagement control
    • Transfer ownership
    • Delete this note
    • Save as template
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Sharing URL Create Help
Create Create new note Create a note from template
Menu
Options
Versions and GitHub Sync Engagement control Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Write
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Engagement control Commenting, Suggest edit, Emoji Reply
Invitee
Publish Note

Publish Note

Everyone on the web can find and read all notes of this public team.
Once published, notes can be searched and viewed by anyone online.
See published notes
Please check the box to agree to the Community Guidelines.
Engagement control
Commenting
Permission
Disabled Forbidden Owners Signed-in users Everyone
Enable
Permission
  • Forbidden
  • Owners
  • Signed-in users
  • Everyone
Suggest edit
Permission
Disabled Forbidden Owners Signed-in users Everyone
Enable
Permission
  • Forbidden
  • Owners
  • Signed-in users
Emoji Reply
Enable
Import from Dropbox Google Drive Gist Clipboard
   owned this note    owned this note      
Published Linked with GitHub
Subscribed
  • Any changes
    Be notified of any changes
  • Mention me
    Be notified of mention me
  • Unsubscribe
Subscribe
# Introduction A security review of the **Oxen Blockchain** smart contract was done by **Cipher Seluths** team, with a focus on the security aspects of the smart contracts implementation. # Disclaimer A smart contract security review can never verify the complete absence of vulnerabilities. This is a time, resource and expertise bound effort where we try to find as many vulnerabilities as possible. we can not guarantee 100% security after the review or even if the review will find any problems with your smart contracts. Subsequent security reviews, bug bounty programs and on-chain monitoring are strongly recommended. # About **Cipher Seluths** **Cipher Seluths** is team of security researchers [**Udsen**](https://code4rena.com/@Udsen) & [**Viraz**](https://twitter.com/Viraz04) who have a good experience participating in codearena contests both solo and as a team & have found multiple vulnerabilities in various protocols. # About **Oxen Blockchain** The Oxen blockchain is a private payments system that enables the creation of many privacy-preserving applications The Oxen blockchain and the $OXEN token are at the heart of Oxen. Together they bring privacy, incentivisation, and decentralisation to the Oxen Blockchain tech stack. # Severity classification | Severity | Impact: High | Impact: Medium | Impact: Low | | ---------------------- | ------------ | -------------- | ----------- | | **Likelihood: High** | Critical | High | Medium | | **Likelihood: Medium** | High | Medium | Low | | **Likelihood: Low** | Medium | Low | Low | **Impact** - the technical, economic and reputation damage of a successful attack **Likelihood** - the chance that a particular vulnerability gets discovered and exploited **Severity** - the overall criticality of the risk # Security Assessment Summary **_review commit hash_ - [e3f6bdc4eef75bce44b44e076cae0e6163b046e4](https://github.com/oxen-io/eth-sn-contracts)** # Detailed Findings ## H-01 Attacker can enable DOS by front running the claiming of network rewards by the user ### Lines of code https://github.com/oxen-io/eth-sn-contracts/blob/master/contracts/ServiceNodeRewards.sol#L128 ## Vulnerability details ### Impact The user managing the service nodes can be front run by an attacker resulting in them unable to claim less rewards/no reward or their claim tx being reverted ### Proof of Concept The `updateRewardsBalance` method is used to set the rewards that can be claimable by the user managing a particular serivice node and then the user can call `claimRewards` method to claim the rewards There is a BLS signature verification that happens before the reward amount is updated ``` BN256G2.G2Point memory signature = BN256G2.G2Point([sigs1,sigs0],[sigs3,sigs2]); bytes memory encodedMessage = abi.encodePacked(rewardTag, recipientAddress, recipientAmount); BN256G2.G2Point memory Hm = BN256G2.hashToG2(BN256G2.hashToField(string(encodedMessage))); if (!Pairing.pairing2(BN256G1.P1(), signature, BN256G1.negate(pubkey), Hm)) revert InvalidBLSSignature(); ``` The issue is in the signed message `msg.sender` isn't checked and there is no mechanism for signature expiry as well which means a attacker can re-use the same sig the user used and set the reward amount as 0 or a lower amount then set by the user before and hence create DOS when a user tries to claim rewards Here is an example which demonstrates the same ``` the user calls updateRewardsBalance and sets the reward amount as 15 tokens and then signs the claim tx the attacker now front runs the claim tx and calls updateRewardsBalance again with the old signature and sets the reward amount for the user as 0 so now when the claim tx initiated by the user reaches finality they don't get any rewards ``` ### Tools Used manual review ### Recommended Mitigation Steps change `bytes memory encodedMessage = abi.encodePacked(rewardTag, recipientAddress, recipientAmount);` to `bytes memory encodedMessage = abi.encodePacked(rewardTag, msg.sender, recipientAmount);` so the signature cannot be used by someone else and also add a mapping to ensure 1 signature can only be used once ## M-01 A whale can get the control of the protocol by having a mojority of stake in the network ### Lines of code https://github.com/oxen-io/eth-sn-contracts/blob/master/contracts/ServiceNodeRewards.sol#L191 ## Vulnerability details ### Impact A single whale can stake large amount of session tokens and create majority of the ServiceNodes thus controlling the key functionalities of the protocol where majority of the ServiceNode signatures are needed for execution and hence create a DOS situtation ### Proof of Concept The `ServiceNodeRewards` contract allows any user to create `any number of ServiceNodes` to the protocol by adding the `BLSPublicKey` by calling the `ServiceNodeRewards.addBLSPublicKey` function with proof of possession BLS signature . But the issue here is that a single whale can stake large amount of session tokens and create majority of the ServiceNodes thus controlling the key functionalities of the protocol where majority of the ServiceNode signatures are needed for execution (Such as `removeBLSPublicKeyWithSignature` and `liquidateBLSPublicKeyWithSignature`). This issue is prevalent specially at the early stages of the protocol where lot of ServiceNodes are not added into the protocol. For example if there are `100 nodes` the `blsNonSignerThreshold` will be `33`. Hence if a single whale user can have the ownership of `67 nodes` he can control the vital operations of the network. When a normal user tries to add more nodes to the network by calling the `addBLSPublicKey` function the whale user can `front run` the same `transaction` with the `same pubkey` and `same signature` and add that node as if it was his. The signature does not have the `msg.sender` and hence this is possible. So when the `normal user's transaction` is executed the following condition in the `ServiceNodeRewards._addBLSPublicKey` will revert. ```solidity uint64 serviceNodeID = serviceNodeIDs[BN256G1.getKeyForG1Point(pubkey)]; if(serviceNodeID != 0) revert BLSPubkeyAlreadyExists(serviceNodeID); ``` The whale can later `remove` this node from the network and get his `funds back` by calling the `removeBLSPublicKeyWithSignature`. As a result the attacker can get the control of the `ServiceNodeRewards` contract since he can single handedly perform `updateRewardBalance`, `Node removal`, `Node liquidation` and can `grief` other users from performing the same operations as well. Further this could `grief the other users` from `removing their nodes` from the protocol and getting thier `staked amount` and `claimable rewards` from the protocol which is `loss of funds` to the other stakers. ### Tools Used manual review ### Recommended Mitigation Steps Hence it is recommended to `limit the number of ServiceNodes` a `single user (msg.sender)` can `add` to the protocol by introducing a upperbound per single address and maintaining a mapping of `address->uint256`. Or even this upperbound can be a percentage of the total nodes (totalNodes) of the network at a given time. In addition `Tokenomics` can be planned in a way, to ensure the cost of a node is sufficiently high so that whales can't achieve 300+ nodes easily. ## M-02 pool liquidation share is rounded down ### Lines of code https://github.com/oxen-io/eth-sn-contracts/blob/master/contracts/ServiceNodeRewards.sol#L357 ## Vulnerability details ### Impact The network get's less liquidation fee than it should ### Proof of Concept Usually during a liquidation event, the liquidation fee is calculated in favour of the protocol i.e the calculation result for the protocol's share is always rounded up, but here the result is rounded down since solidity always rounds the result down during division hence the liquidation share of the netowrk is reduced ``` deposit * poolShareOfLiquidationRatio/ratioSum ``` ### Tools Used manual review ### Recommended Mitigation Steps The calculation of the `protocol share` should be such that the `protocol share` should be rounded up.This can be done by using a `ceil` method of any `math library`. ## M-03 remove BLS public key check is inconsistent ### Lines of code https://github.com/oxen-io/eth-sn-contracts/blob/master/contracts/ServiceNodeRewards.sol#L296 ## Vulnerability details ### Impact bls public key can be removed when `block.timestamp == leaveRequestTimestamp` ### Proof of Concept The comment on top of `removeBLSPublicKeyAfterWaitTime` states that `Removes a BLS public key after a specified wait time` but the check where the current timestamp is same as `leaveRequestTimestamp` the transaction does not revert breaking the invaraint of the netowrk ### Tools Used manual review ### Recommended Mitigation Steps change ` if(block.timestamp < timestamp) revert LeaveRequestTooEarly(serviceNodeID, timestamp, block.timestamp); ` to `if(block.timestamp <= timestamp) revert LeaveRequestTooEarly(serviceNodeID, timestamp, block.timestamp);` # Quality Assuarance ## L-01 Missing pause functionality in the `ServiceNodeRewards` contract ### Impact The network can `remove` the ServiceNode via a liquidation. But if the liquidation is done to decommission fradulent nodes in the event of an attack, the `attacker` can `effect` the `operations` of the protocol till he is decommissioned. ### Tools Used Manual review ### Recommended Mitigation Steps It is recommended to add the `pause functionality` to the contract to `pause` `addition` of the BLS pub keys (ServiceNodes), `removal` of it (in the event of an attack) and `claim reward` operations, as an additional security measure ## L-02 Attacker can enable DOS by preventing a user adding a service node again # Lines of code https://github.com/oxen-io/eth-sn-contracts/blob/master/contracts/ServiceNodeRewards.sol#L191 # Vulnerability details ## Impact The user managing a service node can be front run by an attacker if a service node added by the user is liquidated, then the attacker can use the same public key and hence add the node again, preventing the user from adding it again ## Proof of Concept The `_addBLSPublicKey` method is used to add a new service node, for the proof of possession verification we see ``` BN256G2.G2Point memory Hm = BN256G2.hashToG2(BN256G2.hashToField(string(abi.encodePacked(proofOfPossessionTag, pubkey.X, pubkey.Y)))); ``` where the `pubkey` is derived here `BN256G1.G1Point memory pubkey = BN256G1.G1Point(pkX, pkY);` and `pkX` & `pkY` are function parameters which means that the pub key can reused by anyone since there is no check on the msg.sender during proof of possession verification So consider a scenario where a user adds a new service node and it get's liquidated after some time, now a attacker can add the node again by passing the same `pkX` & `pkY` values and this prevents the user from using their own pubkey and adding the node again ## Tools Used manual review ## Recommended Mitigation Steps change `BN256G2.G2Point memory Hm = BN256G2.hashToG2(BN256G2.hashToField(string(abi.encodePacked(proofOfPossessionTag, pubkey.X, pubkey.Y))));` to `BN256G2.G2Point memory Hm = BN256G2.hashToG2(BN256G2.hashToField(string(abi.encodePacked(proofOfPossessionTag, msg.sender, pubkey.X, pubkey.Y))));` so the signature cannot be used by someone else and also add a mapping to ensure 1 signature can only be used once ## L-03 `leaveRequestTimestamp` is not checked if it is set or not when `removeBLSPublicKeyWithSignature` is called # Lines of code https://github.com/oxen-io/eth-sn-contracts/blob/master/contracts/ServiceNodeRewards.sol#L273 # Vulnerability details ## Impact users can bypass the `initiateRemoveBLSPublicKey` method when removing their code by calling `removeBLSPublicKeyWithSignature` directly ## Proof of Concept `initiateRemoveBLSPublicKey` has a comment which states that `Should be called first and later once the network is happy for node to exist the user should call `removeBLSPublicKeyWithSignature` with a valid BLS signature returned by the network` and hence `leaveRequestTimestamp` is set there the issue is that in `removeBLSPublicKeyWithSignature` method `leaveRequestTimestamp` is not checked so the users essentially can by pass the `initiateRemoveBLSPublicKey` method which breaks the protocol invariant ## Tools Used manual review ## Recommended Mitigation Steps add a check in `removeBLSPublicKeyWithSignature` ``` if(block.timestamp <= leaveRequestTimestamp) revert(); ``` ## L-04 Recommended to use `openzeppelin Ownable2Step` in place of `Ownable` ### Lines of code https://github.com/oxen-io/eth-sn-contracts/blob/master/contracts/ServiceNodeRewards.sol#L6 https://github.com/oxen-io/eth-sn-contracts/blob/master/contracts/ServiceNodeRewards.sol#L12 ### Impact Here the single-step ownership transfer pattern is used. If by mistake the current owner provides an incorrect address as the new owner when calling the `Ownable.transferOwnership` function, then none of the `onlyOwner` methods (seedPublicKeyList, start) of the `ServiceNodeRewards` contract will be callable again. The recommended solution will be to use the `two-step ownership transfer` pattern. Using the two-step process the `new owner` will have to `first claim the ownership` and then the `ownership` will be `transferred to him`. ### Tools Used Manual review ### Recommended Mitigation Steps It is recommended to use `Ownable2Step` of OpenZeppelin and use the `two-step ownership transfer` pattern. ## L-05 `renounceOwnership` function in the inherited openzeppelin `Ownable.sol` contract should be overridden to revert if called ### Impact The Ownable.sol has the` renounceOwnership()` function which is `callable` by the `owner` to `renounce` the `ownership` of the contract to `address(0)` as shown below. ```solidity function renounceOwnership() public virtual onlyOwner { _transferOwnership(address(0)); } ``` This is a `virtual` function and it is `recommended` to `overwrite` this function in the `ServiceNodeRewards.sol` contract such that the `renounceOwnership` will `revert` if `called`. Hence the Owner is not able to renounce the ownerhsip of the contract to address(0). ### Tools Used Manual review ### Recommended Mitigation Steps Recommended to override the `renounceOwnership` function inside the `ServiceNodeRewards` contract such that the transaction will revert if the `owner` calls the `renounceOwnership` function by mistake or intentionally to `sabotage` the contract. ```solidity function renounceOwnership() public override onlyOwner { revert; } ``` ## L-06 `sumAmounts` variable defined and computed in the `ServiceNodeRewards.seedPublicKeyList` function is not used ### Lines of code https://github.com/oxen-io/eth-sn-contracts/blob/master/contracts/ServiceNodeRewards.sol#L367 https://github.com/oxen-io/eth-sn-contracts/blob/master/contracts/ServiceNodeRewards.sol#L382 ### Impact The `ServiceNodeRewards.seedPublicKeyList` function is used to add a list of nodes that are already acting as service nodes in the protocol. And the `staked amounts` for each of the nodes to be added is passed in to the function via `uint256[] calldata amounts` input array. Then each of these nodes are added to the `serviceNodes` linked list and the respective `amounts` are added to the local variable `sumAmounts` as shown below: ```solidity sumAmounts = sumAmounts + amounts[i]; ``` But this `sumAmounts` variable is never used within the scope of the function. The initial assumption was the `sumAmounts` was calculated to transfer the staked amount to the `ServiceNodeRewards` contract after the `seedList` of nodes are added to the `serviceNodes` linked list, at the end of the `ServiceNodeRewards.seedPublicKeyList` function. But since the staked amount transfer for `seedPublicKeyList` nodes are handled by the `foundation` itself currently this `sumAmounts` variable is redundant. ## L-07 Redundant check `serviceNodeRecipient == address(0)` can be omitted in the `ServiceNodeRewards._initiateRemoveBLSPublicKey` function ### Lines of code https://github.com/oxen-io/eth-sn-contracts/blob/master/contracts/ServiceNodeRewards.sol#L249-L251 https://github.com/oxen-io/eth-sn-contracts/blob/master/contracts/ServiceNodeRewards.sol#L259-L260 ### Impact In the `ServiceNodeRewards._initiateRemoveBLSPublicKey` function the following check is redundant, ```solidity if(serviceNodeRecipient == address(0)) revert RecipientAddressNotProvided(serviceNodeID); ``` Due to following reasons: The `_initiateRemoveBLSPublicKey` is called in the `initiateRemoveBLSPublicKey` function, as follows with the `recipient == msg.sender` and the `msg.sender != 0`. ```soldity function initiateRemoveBLSPublicKey(uint64 serviceNodeID) public { _initiateRemoveBLSPublicKey(serviceNodeID, msg.sender); } ``` And the check `serviceNodeRecipient != recipient` in the `_initiateRemoveBLSPublicKey` after the `serviceNodeRecipient == address(0)` check, ensures that the `serviceNodeRecipient !=0` since the `recipient != 0` as explained above. ```solidity if(serviceNodeRecipient != recipient) revert RecipientAddressDoesNotMatch(serviceNodeRecipient, recipient, serviceNodeID); ``` And the `_initiateRemoveBLSPublicKey` is an `internal function` which is called only `once` in the ServiceNodeRewards contract and this contract is `not inherited` by any other contract. ### Tools Used Manual review ### Recommended Mitigation Steps Hence it is recommended to remvoe the redundant check `serviceNodeRecipient == address(0)` in the `ServiceNodeRewards._initiateRemoveBLSPublicKey` since it is anyway checked in the subsequent check. ## L-08 The `ServiceNodeRewards.seedPublicKeyList()` function should only be called before the `hardfork`, but no logic implementation to ensure that ### Lines of code https://github.com/oxen-io/eth-sn-contracts/blob/master/contracts/ServiceNodeRewards.sol#L360 https://github.com/oxen-io/eth-sn-contracts/blob/master/contracts/ServiceNodeRewards.sol#L364-L403 ### Impact The `natspec` comments for the `ServiceNodeRewards.seedPublicKeyList()` function states the following: ```solidity Only should be called before the hardfork by the foundation to ensure the public key list is ready to operate ``` But there is `no logic implementation` in the `seedPublicKeyList` function to `ensure` that this function can `only be called before` the `hardfork`. Hence the `seedPublicKeyList` function can be called after the `hardfork` to add more `ServiceNodes` to the protocol by the `foundation`. ### Tools Used Manual review ### Recommended Mitigation Steps Hence it is recommended to configure a logic in the `seedPublicKeyList()` function to ensure that the function can only be called before the hardfork by the foundation. Any call after the hardfork to the `seedPublicKeyList()` function should revert which is the intended behaviour of the function aligning with the natspec comment. ## L-09 The `totalNodes` state variable is not incremented inside the `for loop` of the `ServiceNodeRewards.seedPublicKeyList` function ### Lines of code https://github.com/oxen-io/eth-sn-contracts/blob/master/contracts/ServiceNodeRewards.sol#L401 https://github.com/oxen-io/eth-sn-contracts/blob/master/contracts/ServiceNodeRewards.sol#L371-L396 ### Impact The `ServiceNodeRewards.seedPublicKeyList` function is used by the `foundation` to add a list of nodes that are `already acting` as `service nodes` in the `protocol`. In the `logic implementation` of the `seedPublicKeyList` function the `Seed ServiceNodes` are added one by one iterating through a `for loop`. But the issue is the `totalNodes` state variable is not incremented inside the `for loop` but only increments after the `for loop` ends. But the `updateServiceNodesLength` function implementation suggests that the `totalNodes = serviceNodesLength()` which means the number of service nodes added to the protocol is equal to the length of the `serviceNodes` linked list. As a result irrespecitve of number of `Seed ServiceNodes` added by the foundation via the `seedPublicKeyList` function the `totalNodes = 1` after the execution which is erroneous and a broken state. ### Tools Used Manual review ### Recommended Mitigation Steps Hence it is recommended to increment the `totalNodes` inside the `for loop` which is used to add `Seed ServiceNodes` in the `ServiceNodeRewards.seedPublicKeyList` function. # Informational ## I-01 Typo in the natspec comment ### Lines of code https://github.com/oxen-io/eth-sn-contracts/blob/master/contracts/ServiceNodeRewards.sol#L247 ### Impact There is a type in the following natspec comment in the `exist` word and should be `exit` instead. ```solidity Should be called first and later once the network is happy for node to exist the user should call removeBLSPublicKeyWithSignature with a valid BLS signature returned by the network ``` ### Tools Used Manual review ### Recommended Mitigation Steps Typo exist in the above mentioned natspec comment should be corrected as `exit`. Proper natspec commenting will improve the code readability and understanding for the developers and auditors. # Gas Optimisations ## G-01 Use compiler version >= 0.8.22 or use `unchecked` in for loops The for loops used have the counter operation together with the counter initialisation which costs extra gas everytime there is a counter increment so it is recomended to either use a compiler version >= 0.8.22 or do the counter operation in the unchecked block i.e ``` for(uint256 i; i < length;) { . . . unchecked { += i; } } ``` ## G-02 Variables only initialized in the constructor should be marked as immutable `stakingRequirement, liquidatorRewardRatio, poolShareOfLiquidationRatio & recipientRatio` are only initialized in the constructor and not updated anywhere else so these variables should be marked as immutable to save gas # Other Suggestions We strongly recommend that the test coverage of the contracts should be close to 100%, static analysers like slither should also be used for marking and fixing any issues identified there.

Import from clipboard

Paste your webpage below. It will be converted to Markdown.

Advanced permission required

Your current role can only read. Ask the system administrator to acquire write and comment permission.

This team is disabled

Sorry, this team is disabled. You can't edit this note.

This note is locked

Sorry, only owner can edit this note.

Reach the limit

Sorry, you've reached the max length this note can be.
Please reduce the content or divide it to more notes, thank you!

Import from Gist

Import from Snippet

or

Export to Snippet

Are you sure?

Do you really want to delete this note?
All users will lose their connection.

Create a note from template

Create a note from template

Oops...
This template is not available.
Upgrade
All
  • All
  • Team
No template found.

Create custom template

Upgrade

Delete template

Do you really want to delete this template?
Turn this template into a regular note and keep its content, versions, and comments.

This page need refresh

You have an incompatible client version.
Refresh to update.
New version available!
See releases notes here
Refresh to enjoy new features.
Your user state has changed.
Refresh to load new user state.

Sign in

Forgot password

or

By clicking below, you agree to our terms of service.

Sign in via Facebook Sign in via Twitter Sign in via GitHub Sign in via Dropbox Sign in with Wallet
Wallet ( )
Connect another wallet

New to HackMD? Sign up

Help

  • English
  • 中文
  • Français
  • Deutsch
  • 日本語
  • Español
  • Català
  • Ελληνικά
  • Português
  • italiano
  • Türkçe
  • Русский
  • Nederlands
  • hrvatski jezik
  • język polski
  • Українська
  • हिन्दी
  • svenska
  • Esperanto
  • dansk

Documents

Help & Tutorial

How to use Book mode

How to use Slide mode

API Docs

Edit in VSCode

Install browser extension

Get in Touch

Feedback

Discord

Send us email

Resources

Releases

Pricing

Blog

Policy

Terms

Privacy

Cheatsheet

Syntax Example Reference
# Header Header 基本排版
- Unordered List
  • Unordered List
1. Ordered List
  1. Ordered List
- [ ] Todo List
  • Todo List
> Blockquote
Blockquote
**Bold font** Bold font
*Italics font* Italics font
~~Strikethrough~~ Strikethrough
19^th^ 19th
H~2~O H2O
++Inserted text++ Inserted text
==Marked text== Marked text
[link text](https:// "title") Link
![image alt](https:// "title") Image
`Code` Code 在筆記中貼入程式碼
```javascript
var i = 0;
```
var i = 0;
:smile: :smile: Emoji list
{%youtube youtube_id %} Externals
$L^aT_eX$ LaTeX
:::info
This is a alert area.
:::

This is a alert area.

Versions and GitHub Sync
Get Full History Access

  • Edit version name
  • Delete

revision author avatar     named on  

More Less

No updates to save
Compare
    Choose a version
    No search result
    Version not found
Sign in to link this note to GitHub
Learn more
This note is not linked with GitHub
 

Feedback

Submission failed, please try again

Thanks for your support.

On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

Please give us some advice and help us improve HackMD.

 

Thanks for your feedback

Remove version name

Do you want to remove this version name and description?

Transfer ownership

Transfer to
    Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

      Link with GitHub

      Please authorize HackMD on GitHub
      • Please sign in to GitHub and install the HackMD app on your GitHub repo.
      • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
      Learn more  Sign in to GitHub

      Push the note to GitHub Push to GitHub Pull a file from GitHub

        Authorize again
       

      Choose which file to push to

      Select repo
      Refresh Authorize more repos
      Select branch
      Select file
      Select branch
      Choose version(s) to push
      • Save a new version and push
      • Choose from existing versions
      Include title and tags
      Available push count

      Pull from GitHub

       
      File from GitHub
      File from HackMD

      GitHub Link Settings

      File linked

      Linked by
      File path
      Last synced branch
      Available push count

      Danger Zone

      Unlink
      You will no longer receive notification when GitHub file changes after unlink.

      Syncing

      Push failed

      Push successfully