Skip to content

Commit

Permalink
Add lockup endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
PierreLeGuen committed Oct 19, 2023
1 parent 8f46e1f commit bdd6592
Show file tree
Hide file tree
Showing 8 changed files with 583 additions and 16 deletions.
15 changes: 14 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ lru = "0.11.0"
governor = "0.6.0"
tracing-loki = "0.2.4"
reqwest = "0.11.22"
uint = { version = "0.8.3", default-features = false }

[dev-dependencies]
axum-test-helper = "0.3.0"
3 changes: 3 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ pub fn get_accounts_and_lockups(accounts: &str) -> HashSet<(String, Option<Strin
.collect();

for a in accounts.clone() {
if a.0.ends_with(".lockup.near") {
continue;
}
let lockup_account = get_associated_lockup(&a.0, "near");
accounts.insert((lockup_account, Some(a.0.clone())));
}
Expand Down
196 changes: 196 additions & 0 deletions src/lockup/l.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
use std::time::Duration;

use anyhow::Context;

use near_jsonrpc_client::{methods, JsonRpcClient};
use near_jsonrpc_primitives::types::query::QueryResponseKind;
use near_primitives::hash::CryptoHash;
use near_primitives::types::{AccountId, BlockHeight, BlockId, BlockReference};
use near_primitives::views::QueryRequest;
use near_sdk::borsh::BorshDeserialize;
use near_sdk::json_types::{U128, U64};
use tracing::info;

use super::lockup_types::{
LockupContract, TransfersInformation, VestingInformation, VestingSchedule, WrappedBalance, U256,
};

// The timestamp (nanos) when transfers were enabled in the Mainnet after community voting
// Tuesday, 13 October 2020 18:38:58.293
pub(super) const TRANSFERS_ENABLED: Duration = Duration::from_nanos(1602614338293769340);
const CIRCULATING_SUPPLY: &str = "circulating_supply";

pub(crate) async fn get_lockup_contract_state(
rpc_client: &JsonRpcClient,
account_id: &AccountId,
block_height: &BlockHeight,
) -> anyhow::Result<LockupContract> {
let block_reference = BlockReference::BlockId(BlockId::Height(*block_height));
let request = QueryRequest::ViewState {
account_id: account_id.clone(),
prefix: vec![].into(),
include_proof: false,
};
let query = methods::query::RpcQueryRequest {
block_reference,
request,
};

let state_response = rpc_client.call(query).await.with_context(|| {
format!(
"Failed to deliver ViewState for lockup contract {}, block_height {}",
account_id, block_height
)
})?;

let view_state_result = match state_response.kind {
QueryResponseKind::ViewState(state) => state,
_ => {
anyhow::bail!(
"Failed to extract ViewState response for lockup contract {}, block_height {}",
account_id,
block_height
)
}
};

let view_state = view_state_result.values.get(0).with_context(|| {
format!(
"Failed to find encoded lockup contract for {}, block_height {}",
account_id, block_height
)
})?;

let mut state = LockupContract::try_from_slice(&view_state.value)
.with_context(|| format!("Failed to construct LockupContract for {}", account_id))?;

// If owner of the lockup account didn't call the
// `check_transfers_vote` contract method we won't be able to
// get proper information based on timestamp, that's why we inject
// the `transfer_timestamp` which is phase2 timestamp
state.lockup_information.transfers_information = TransfersInformation::TransfersEnabled {
transfers_timestamp: U64(TRANSFERS_ENABLED.as_nanos() as u64),
};
Ok(state)
}

// The lockup contract implementation had a bug that affected lockup start date.
// https://github.com/near/core-contracts/pull/136
// For each contract, we should choose the logic based on the binary version of the contract
pub(crate) fn is_bug_inside_contract(code_hash: &CryptoHash, account_id: &AccountId) -> bool {
match &*code_hash.to_string() {
// The first implementation, with the bug
"3kVY9qcVRoW3B5498SMX6R3rtSLiCdmBzKs7zcnzDJ7Q" => true,
// We have 6 lockups created at 6th of April 2021, assume it's buggy
"DiC9bKCqUHqoYqUXovAnqugiuntHWnM3cAc7KrgaHTu" => true,
// Another 5 lockups created in May/June 2021, assume they are OK
"Cw7bnyp4B6ypwvgZuMmJtY6rHsxP2D4PC8deqeJ3HP7D" => false,
// Most recent contracts
"4Pfw2RU6e35dUsHQQoFYfwX8KFFvSRNwMSNLXuSFHXrC" => false,
"3skHaUtj85RPdUZwx6M4Jp4PfC9qJHqnsyuWLtuq2xBT" => false,
_ => {
info!(
target: CIRCULATING_SUPPLY,
"Assuming contract {} for account {} is not buggy", code_hash, account_id
);
false
}
}
}

// This is almost a copy of https://github.com/near/core-contracts/blob/master/lockup/src/getters.rs#L64
impl LockupContract {
/// Returns the amount of tokens that are locked in the account due to lockup or vesting.
pub fn get_locked_amount(&self, timestamp: u64, has_bug: bool) -> WrappedBalance {
let lockup_amount = self.lockup_information.lockup_amount;
if let TransfersInformation::TransfersEnabled {
transfers_timestamp,
} = &self.lockup_information.transfers_information
{
let lockup_timestamp = std::cmp::max(
transfers_timestamp
.0
.saturating_add(self.lockup_information.lockup_duration),
self.lockup_information.lockup_timestamp.unwrap_or(0),
);
let block_timestamp = timestamp;
if lockup_timestamp <= block_timestamp {
let unreleased_amount =
if let Some(release_duration) = self.lockup_information.release_duration {
let start_lockup = if has_bug {
transfers_timestamp.0
} else {
lockup_timestamp
};
let end_timestamp = start_lockup.saturating_add(release_duration);
if block_timestamp >= end_timestamp {
// Everything is released
0
} else {
let time_left = U256::from(end_timestamp - block_timestamp);
let unreleased_amount = U256::from(lockup_amount) * time_left
/ U256::from(release_duration);
// The unreleased amount can't be larger than lockup_amount because the
// time_left is smaller than total_time.
unreleased_amount.as_u128()
}
} else {
0
};

let unvested_amount = match &self.vesting_information {
VestingInformation::VestingSchedule(vs) => {
self.get_unvested_amount(vs.clone(), block_timestamp)
}
VestingInformation::Terminating(terminating) => terminating.unvested_amount,
// Vesting is private, so we can assume the vesting started before lockup date.
_ => U128(0),
};
return std::cmp::max(
unreleased_amount
.saturating_sub(self.lockup_information.termination_withdrawn_tokens),
unvested_amount.0,
)
.into();
}
}
// The entire balance is still locked before the lockup timestamp.
(lockup_amount - self.lockup_information.termination_withdrawn_tokens).into()
}

/// Returns the amount of tokens that are locked in this account due to vesting schedule.
/// Takes raw vesting schedule, in case the internal vesting schedule is private.
pub fn get_unvested_amount(
&self,
vesting_schedule: VestingSchedule,
block_timestamp: u64,
) -> WrappedBalance {
let lockup_amount = self.lockup_information.lockup_amount;
match &self.vesting_information {
VestingInformation::Terminating(termination_information) => {
termination_information.unvested_amount
}
VestingInformation::None => U128::from(0),
_ => {
if block_timestamp < vesting_schedule.cliff_timestamp.0 {
// Before the cliff, nothing is vested
lockup_amount.into()
} else if block_timestamp >= vesting_schedule.end_timestamp.0 {
// After the end, everything is vested
0.into()
} else {
// cannot overflow since block_timestamp < vesting_schedule.end_timestamp
let time_left = U256::from(vesting_schedule.end_timestamp.0 - block_timestamp);
// The total time is positive. Checked at the contract initialization.
let total_time = U256::from(
vesting_schedule.end_timestamp.0 - vesting_schedule.start_timestamp.0,
);
let unvested_amount = U256::from(lockup_amount) * time_left / total_time;
// The unvested amount can't be larger than lockup_amount because the
// time_left is smaller than total_time.
unvested_amount.as_u128().into()
}
}
}
}
}
Loading

0 comments on commit bdd6592

Please sign in to comment.