-
Notifications
You must be signed in to change notification settings - Fork 49
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(staking): add BTC support #60
Changes from all commits
32e1970
d94d08d
eb6bb86
b507eeb
7ff9eed
7326c39
62562dc
8dd1509
bb3a929
541506f
e498bdb
bc0da61
e783534
784f143
f5a6576
5e56ffd
ff6eeeb
626e970
c4ccaf9
32658e1
13e3680
8d4970f
967401c
d32bb85
3a466b3
25fdfa7
bbb2149
3f8585e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -8,17 +8,25 @@ import "@zetachain/toolkit/contracts/BytesHelperLib.sol"; | |||||
|
||||||
contract Staking is ERC20, zContract { | ||||||
error SenderNotSystemContract(); | ||||||
error WrongChain(); | ||||||
error NotAuthorizedToClaim(); | ||||||
error WrongChain(uint256 chainID); | ||||||
error UnknownAction(uint8 action); | ||||||
error Overflow(); | ||||||
error Underflow(); | ||||||
error WrongAmount(); | ||||||
error NotAuthorized(); | ||||||
error NoRewardsToClaim(); | ||||||
|
||||||
SystemContract public immutable systemContract; | ||||||
uint256 public immutable chainID; | ||||||
uint256 constant BITCOIN = 18332; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it would be more visual grouping immutable and constant together, no public between There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||||||
|
||||||
mapping(address => uint256) public stakes; | ||||||
mapping(address => address) public beneficiaries; | ||||||
mapping(address => uint256) public lastStakeTime; | ||||||
uint256 public rewardRate = 1; | ||||||
|
||||||
mapping(address => uint256) public stake; | ||||||
mapping(address => bytes) public withdraw; | ||||||
mapping(address => address) public beneficiary; | ||||||
mapping(address => uint256) public lastStakeTime; | ||||||
|
||||||
constructor( | ||||||
string memory name_, | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Same comment for variables below There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. that's the old standard. in current solidity standard _ at the end means avoid name collision and at the beginning is only for private names There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok, didn't know that, I've never seen this standard |
||||||
string memory symbol_, | ||||||
|
@@ -29,74 +37,126 @@ contract Staking is ERC20, zContract { | |||||
chainID = chainID_; | ||||||
} | ||||||
|
||||||
modifier onlySystem() { | ||||||
require( | ||||||
msg.sender == address(systemContract), | ||||||
"Only system contract can call this function" | ||||||
); | ||||||
_; | ||||||
} | ||||||
|
||||||
function bytesToBech32Bytes( | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It looks like this one could be There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||||||
bytes calldata data, | ||||||
uint256 offset | ||||||
) internal pure returns (bytes memory) { | ||||||
bytes memory bech32Bytes = new bytes(42); | ||||||
for (uint i = 0; i < 42; i++) { | ||||||
bech32Bytes[i] = data[i + offset]; | ||||||
} | ||||||
|
||||||
return bech32Bytes; | ||||||
} | ||||||
|
||||||
function onCrossChainCall( | ||||||
zContext calldata context, | ||||||
address zrc20, | ||||||
uint256 amount, | ||||||
bytes calldata message | ||||||
) external override { | ||||||
if (msg.sender != address(systemContract)) { | ||||||
revert SenderNotSystemContract(); | ||||||
) external override onlySystem { | ||||||
if (chainID != context.chainID) { | ||||||
revert WrongChain(context.chainID); | ||||||
} | ||||||
|
||||||
address acceptedZRC20 = systemContract.gasCoinZRC20ByChainId(chainID); | ||||||
if (zrc20 != acceptedZRC20) revert WrongChain(); | ||||||
|
||||||
address staker = BytesHelperLib.bytesToAddress(context.origin, 0); | ||||||
address beneficiary = abi.decode(message, (address)); | ||||||
|
||||||
stakeZRC(staker, beneficiary, amount); | ||||||
uint8 action = chainID == BITCOIN | ||||||
? uint8(message[0]) | ||||||
: abi.decode(message, (uint8)); | ||||||
|
||||||
if (action == 1) { | ||||||
stakeZRC(staker, amount); | ||||||
} else if (action == 2) { | ||||||
unstakeZRC(staker); | ||||||
} else if (action == 3) { | ||||||
setBeneficiary(staker, message); | ||||||
} else if (action == 4) { | ||||||
setWithdraw(staker, message, context.origin); | ||||||
} else { | ||||||
revert UnknownAction(action); | ||||||
} | ||||||
} | ||||||
|
||||||
function stakeZRC( | ||||||
address staker, | ||||||
address beneficiary, | ||||||
uint256 amount | ||||||
) internal { | ||||||
stakes[staker] += amount; | ||||||
if (beneficiaries[staker] == address(0)) { | ||||||
beneficiaries[staker] = beneficiary; | ||||||
} | ||||||
lastStakeTime[staker] = block.timestamp; | ||||||
function stakeZRC(address staker, uint256 amount) internal { | ||||||
stake[staker] += amount; | ||||||
if (stake[staker] < amount) revert Overflow(); | ||||||
|
||||||
lastStakeTime[staker] = block.timestamp; | ||||||
updateRewards(staker); | ||||||
} | ||||||
|
||||||
function updateRewards(address staker) internal { | ||||||
uint256 timeDifference = block.timestamp - lastStakeTime[staker]; | ||||||
uint256 rewardAmount = timeDifference * stakes[staker] * rewardRate; | ||||||
uint256 rewardAmount = queryRewards(staker); | ||||||
|
||||||
_mint(beneficiaries[staker], rewardAmount); | ||||||
_mint(beneficiary[staker], rewardAmount); | ||||||
lastStakeTime[staker] = block.timestamp; | ||||||
} | ||||||
|
||||||
function claimRewards(address staker) external { | ||||||
if (beneficiaries[staker] != msg.sender) { | ||||||
revert NotAuthorizedToClaim(); | ||||||
} | ||||||
function unstakeZRC(address staker) internal { | ||||||
uint256 amount = stake[staker]; | ||||||
|
||||||
updateRewards(staker); | ||||||
} | ||||||
|
||||||
function unstakeZRC(uint256 amount) external { | ||||||
updateRewards(msg.sender); | ||||||
address zrc20 = systemContract.gasCoinZRC20ByChainId(chainID); | ||||||
(, uint256 gasFee) = IZRC20(zrc20).withdrawGasFee(); | ||||||
|
||||||
require(stakes[msg.sender] >= amount, "Insufficient staked balance"); | ||||||
if (amount < gasFee) revert WrongAmount(); | ||||||
|
||||||
address zrc20 = systemContract.gasCoinZRC20ByChainId(chainID); | ||||||
bytes memory recipient = withdraw[staker]; | ||||||
|
||||||
(address gasZRC20, uint256 gasFee) = IZRC20(zrc20).withdrawGasFee(); | ||||||
stake[staker] = 0; | ||||||
|
||||||
IZRC20(zrc20).approve(zrc20, gasFee); | ||||||
IZRC20(zrc20).withdraw(abi.encodePacked(msg.sender), amount - gasFee); | ||||||
IZRC20(zrc20).withdraw(recipient, amount - gasFee); | ||||||
|
||||||
if (stake[staker] > amount) revert Underflow(); | ||||||
|
||||||
lastStakeTime[staker] = block.timestamp; | ||||||
} | ||||||
|
||||||
function setBeneficiary(address staker, bytes calldata message) internal { | ||||||
address beneficiaryAddress; | ||||||
if (chainID == BITCOIN) { | ||||||
beneficiaryAddress = BytesHelperLib.bytesToAddress(message, 1); | ||||||
} else { | ||||||
(, beneficiaryAddress) = abi.decode(message, (uint8, address)); | ||||||
} | ||||||
beneficiary[staker] = beneficiaryAddress; | ||||||
} | ||||||
|
||||||
stakes[msg.sender] -= amount; | ||||||
lastStakeTime[msg.sender] = block.timestamp; | ||||||
function setWithdraw( | ||||||
address staker, | ||||||
bytes calldata message, | ||||||
bytes memory origin | ||||||
) internal { | ||||||
bytes memory withdrawAddress; | ||||||
if (chainID == BITCOIN) { | ||||||
withdrawAddress = bytesToBech32Bytes(message, 1); | ||||||
} else { | ||||||
withdrawAddress = origin; | ||||||
} | ||||||
withdraw[staker] = withdrawAddress; | ||||||
} | ||||||
|
||||||
function queryRewards(address account) public view returns (uint256) { | ||||||
uint256 timeDifference = block.timestamp - lastStakeTime[account]; | ||||||
uint256 rewardAmount = timeDifference * stakes[account] * rewardRate; | ||||||
function queryRewards(address staker) public view returns (uint256) { | ||||||
uint256 timeDifference = block.timestamp - lastStakeTime[staker]; | ||||||
uint256 rewardAmount = timeDifference * stake[staker] * rewardRate; | ||||||
return rewardAmount; | ||||||
} | ||||||
|
||||||
function claimRewards(address staker) external { | ||||||
if (beneficiary[staker] != msg.sender) revert NotAuthorized(); | ||||||
uint256 rewardAmount = queryRewards(staker); | ||||||
if (rewardAmount <= 0) revert NoRewardsToClaim(); | ||||||
updateRewards(staker); | ||||||
} | ||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import { ethers } from "ethers"; | ||
|
||
export const convertToHexAddress = (address: string): string => { | ||
let addr: string; | ||
try { | ||
// Check if it's a valid hex address | ||
addr = ethers.utils.getAddress(address); | ||
} catch (e) { | ||
// If not, try to convert it to an address from bech32 | ||
addr = ("0x" + Buffer.from(address).toString("hex")).slice(0, 42); | ||
} | ||
return addr; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import { task } from "hardhat/config"; | ||
import { HardhatRuntimeEnvironment } from "hardhat/types"; | ||
import { utils } from "ethers"; | ||
|
||
const main = async (args: any, hre: HardhatRuntimeEnvironment) => { | ||
const dataTypes = ["bytes"]; | ||
const values = [utils.toUtf8Bytes(args.address)]; | ||
|
||
const encodedData = utils.solidityPack(dataTypes, values); | ||
console.log(`Encoded: ${encodedData}`); | ||
console.log(`context.origin: ${encodedData.slice(0, 42)}`); | ||
}; | ||
|
||
task( | ||
"address", | ||
"Encode a Bitcoin bech32 address to hex", | ||
main | ||
).addPositionalParam("address"); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import { task } from "hardhat/config"; | ||
import { HardhatRuntimeEnvironment } from "hardhat/types"; | ||
import { parseEther } from "@ethersproject/units"; | ||
import { getAddress } from "@zetachain/protocol-contracts"; | ||
import { prepareData, trackCCTX } from "@zetachain/toolkit/helpers"; | ||
|
||
const main = async (args: any, hre: HardhatRuntimeEnvironment) => { | ||
const [signer] = await hre.ethers.getSigners(); | ||
console.log(`🔑 Using account: ${signer.address}\n`); | ||
|
||
const data = prepareData( | ||
args.contract, | ||
["uint8", "address"], | ||
["3", args.beneficiary] | ||
); | ||
const to = getAddress("tss", hre.network.name); | ||
const value = parseEther("0"); | ||
|
||
const tx = await signer.sendTransaction({ data, to, value }); | ||
console.log(` | ||
🚀 Successfully broadcasted a token transfer transaction on ${hre.network.name} network. | ||
📝 Transaction hash: ${tx.hash} | ||
`); | ||
await trackCCTX(tx.hash); | ||
}; | ||
|
||
task( | ||
"set-beneficiary", | ||
"Set the address on ZetaChain which will be allowed to claim staking rewards", | ||
main | ||
) | ||
.addParam("contract", "The address of the contract on ZetaChain") | ||
.addPositionalParam("beneficiary", "The address of the beneficiary"); |
This file was deleted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think immutable could be written in full uppercase.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
that's not the solidity standard and is not the one with use. is more important to be consistent with all the contracts.