Skip to content

Commit

Permalink
fix: add ccip017 contract, model, and test
Browse files Browse the repository at this point in the history
  • Loading branch information
whoabuddy committed Sep 1, 2023
1 parent 8106042 commit 7dad7d6
Show file tree
Hide file tree
Showing 5 changed files with 886 additions and 0 deletions.
5 changes: 5 additions & 0 deletions Clarinet.toml
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,11 @@ path = "contracts/proposals/ccip014-pox-3-v2.clar"
clarity_version = 2
epoch = 2.4

[contracts.ccip017]
path = "contracts/proposals/ccip017-extend-direct-execute-sunset-period.clar"
clarity_version = 2
epoch = 2.4

# CITYCOINS PROTOCOL TRAITS

[contracts.extension-trait]
Expand Down
265 changes: 265 additions & 0 deletions contracts/proposals/ccip017-extend-direct-execute-sunset-period.clar
Original file line number Diff line number Diff line change
@@ -0,0 +1,265 @@
;; TRAITS

(impl-trait .proposal-trait.proposal-trait)
(impl-trait .ccip-015-trait.ccip-015-trait)

;; ERRORS

(define-constant ERR_PANIC (err u1700))
(define-constant ERR_VOTED_ALREADY (err u1701))
(define-constant ERR_NOTHING_STACKED (err u1702))
(define-constant ERR_USER_NOT_FOUND (err u1703))
(define-constant ERR_PROPOSAL_NOT_ACTIVE (err u1704))
(define-constant ERR_PROPOSAL_STILL_ACTIVE (err u1705))
(define-constant ERR_NO_CITY_ID (err u1706))
(define-constant ERR_VOTE_FAILED (err u1707))

;; CONSTANTS

(define-constant SELF (as-contract tx-sender))
(define-constant CCIP_017 {
name: "Extend Direct Execute Sunset Period",
link: "https://github.com/citycoins/governance/blob/feat/add-ccip-017/ccips/ccip-017/ccip-017-extend-direct-execute-sunset-period.md",
hash: "7ddbf6152790a730faa059b564a8524abc3c70d3",
})
(define-constant SUNSET_BLOCK u147828)

(define-constant VOTE_SCALE_FACTOR (pow u10 u16)) ;; 16 decimal places
(define-constant MIA_SCALE_BASE (pow u10 u4)) ;; 4 decimal places
(define-constant MIA_SCALE_FACTOR u8823) ;; 0.8823 or 88.23%
;; MIA votes scaled to make 1 MIA = 1 NYC
;; full calculation available in CCIP-017

;; DATA VARS

;; vote block heights
(define-data-var voteActive bool true)
(define-data-var voteStart uint u0)
(define-data-var voteEnd uint u0)

(var-set voteStart block-height)

;; vote tracking
(define-data-var yesVotes uint u0)
(define-data-var yesTotal uint u0)
(define-data-var noVotes uint u0)
(define-data-var noTotal uint u0)

;; DATA MAPS

(define-map UserVotes
uint ;; user ID
{ ;; vote
vote: bool,
mia: uint,
nyc: uint,
total: uint,
}
)

;; PUBLIC FUNCTIONS

(define-public (execute (sender principal))
(begin
;; check vote complete/passed
(try! (is-executable))
;; update vote variables
(var-set voteEnd block-height)
(var-set voteActive false)
;; extend sunset height in ccd001-direct-execute
(try! (contract-call? .ccd001-direct-execute set-sunset-block SUNSET_BLOCK))
(ok true)
)
)

(define-public (vote-on-proposal (vote bool))
(let
(
(miaId (unwrap! (contract-call? .ccd004-city-registry get-city-id "mia") ERR_NO_CITY_ID))
(nycId (unwrap! (contract-call? .ccd004-city-registry get-city-id "nyc") ERR_NO_CITY_ID))
(voterId (unwrap! (contract-call? .ccd003-user-registry get-user-id contract-caller) ERR_USER_NOT_FOUND))
(voterRecord (map-get? UserVotes voterId))
)
;; check that proposal is active
(asserts! (var-get voteActive) ERR_PROPOSAL_NOT_ACTIVE)
;; check if vote record exists
(match voterRecord record
;; if the voterRecord exists
(begin
;; check vote is not the same as before
(asserts! (not (is-eq (get vote record) vote)) ERR_VOTED_ALREADY)
;; record the new vote for the user
(map-set UserVotes voterId
(merge record { vote: vote })
)
;; update the overall vote totals
(if vote
(begin
(var-set yesVotes (+ (var-get yesVotes) u1))
(var-set yesTotal (+ (var-get yesTotal) (get total record)))
(var-set noVotes (- (var-get noVotes) u1))
(var-set noTotal (- (var-get noTotal) (get total record)))
)
(begin
(var-set yesVotes (- (var-get yesVotes) u1))
(var-set yesTotal (- (var-get yesTotal) (get total record)))
(var-set noVotes (+ (var-get noVotes) u1))
(var-set noTotal (+ (var-get noTotal) (get total record)))
)
)
)
;; if the voterRecord does not exist
(let
(
(scaledVoteMia (default-to u0 (get-mia-vote miaId voterId true)))
(scaledVoteNyc (default-to u0 (get-nyc-vote nycId voterId true)))
(voteMia (scale-down scaledVoteMia))
(voteNyc (scale-down scaledVoteNyc))
(voteTotal (+ voteMia voteNyc))
)
;; record the vote for the user
(map-insert UserVotes voterId {
vote: vote,
mia: voteMia,
nyc: voteNyc,
total: voteTotal,
})
;; update the overall vote totals
(if vote
(begin
(var-set yesVotes (+ (var-get yesVotes) u1))
(var-set yesTotal (+ (var-get yesTotal) voteTotal))
)
(begin
(var-set noVotes (+ (var-get noVotes) u1))
(var-set noTotal (+ (var-get noTotal) voteTotal))
)
)
)
)
;; print voter information
(print (map-get? UserVotes voterId))
;; print vote totals
(print (get-vote-totals))
(ok true)
)
)

;; READ ONLY FUNCTIONS

(define-read-only (is-executable)
(begin
;; check that there is at least one vote
(asserts! (or (> (var-get yesVotes) u0) (> (var-get noVotes) u0)) ERR_VOTE_FAILED)
;; check that yes total is more than no total
(asserts! (> (var-get yesTotal) (var-get noTotal)) ERR_VOTE_FAILED)
(ok true)
)
)

(define-read-only (is-vote-active)
(some (var-get voteActive))
)

(define-read-only (get-proposal-info)
(some CCIP_017)
)

(define-read-only (get-vote-period)
(if (and
(> (var-get voteStart) u0)
(> (var-get voteEnd) u0))
;; if both are set, return values
(some {
startBlock: (var-get voteStart),
endBlock: (var-get voteEnd),
length: (- (var-get voteEnd) (var-get voteStart))
})
;; else return none
none
)
)

(define-read-only (get-vote-totals)
(some {
yesVotes: (var-get yesVotes),
yesTotal: (var-get yesTotal),
noVotes: (var-get noVotes),
noTotal: (var-get noTotal)
})
)

(define-read-only (get-voter-info (id uint))
(map-get? UserVotes id)
)

;; MIA vote calculation
;; returns (some uint) or (none)
;; optionally scaled by VOTE_SCALE_FACTOR (10^6)
(define-read-only (get-mia-vote (cityId uint) (userId uint) (scaled bool))
(let
(
;; MAINNET: MIA cycle 64 / first block BTC 800,450 STX 114,689
;; cycle 2 / u4500 used in tests
(cycle64Hash (unwrap! (get-block-hash u4500) none))
(cycle64Data (at-block cycle64Hash (contract-call? .ccd007-citycoin-stacking get-stacker cityId u2 userId)))
(cycle64Amount (get stacked cycle64Data))
;; MAINNET: MIA cycle 65 / first block BTC 804,649 STX 118,282
;; cycle 3 / u6600 used in tests
(cycle65Hash (unwrap! (get-block-hash u6600) none))
(cycle65Data (at-block cycle65Hash (contract-call? .ccd007-citycoin-stacking get-stacker cityId u3 userId)))
(cycle65Amount (get stacked cycle65Data))
;; MIA vote calculation
(avgStacked (/ (+ (scale-up cycle64Amount) (scale-up cycle65Amount)) u2))
(scaledVote (/ (* avgStacked MIA_SCALE_FACTOR) MIA_SCALE_BASE))
)
;; check that at least one value is positive
(asserts! (or (> cycle64Amount u0) (> cycle65Amount u0)) none)
;; return scaled or unscaled value
(if scaled (some scaledVote) (some (/ scaledVote VOTE_SCALE_FACTOR)))
)
)

;; NYC vote calculation
;; returns (some uint) or (none)
;; optionally scaled by VOTE_SCALE_FACTOR (10^6)
(define-read-only (get-nyc-vote (cityId uint) (userId uint) (scaled bool))
(let
(
;; NYC cycle 64 / first block BTC 800,450 STX 114,689
;; cycle 2 / u4500 used in tests
(cycle64Hash (unwrap! (get-block-hash u4500) none))
(cycle64Data (at-block cycle64Hash (contract-call? .ccd007-citycoin-stacking get-stacker cityId u2 userId)))
(cycle64Amount (get stacked cycle64Data))
;; NYC cycle 65 / first block BTC 804,649 STX 118,282
;; cycle 3 / u6600 used in tests
(cycle65Hash (unwrap! (get-block-hash u6600) none))
(cycle65Data (at-block cycle65Hash (contract-call? .ccd007-citycoin-stacking get-stacker cityId u3 userId)))
(cycle65Amount (get stacked cycle65Data))
;; NYC vote calculation
(scaledVote (/ (+ (scale-up cycle64Amount) (scale-up cycle65Amount)) u2))
)
;; check that at least one value is positive
(asserts! (or (> cycle64Amount u0) (> cycle65Amount u0)) none)
;; return scaled or unscaled value
(if scaled (some scaledVote) (some (/ scaledVote VOTE_SCALE_FACTOR)))
)
)

;; PRIVATE FUNCTIONS

;; get block hash by height
(define-private (get-block-hash (blockHeight uint))
(get-block-info? id-header-hash blockHeight)
)

;; CREDIT: ALEX math-fixed-point-16.clar

(define-private (scale-up (a uint))
(* a VOTE_SCALE_FACTOR)
)

(define-private (scale-down (a uint))
(/ a VOTE_SCALE_FACTOR)
)

Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { PROPOSALS } from "../../utils/common.ts";
import { Chain, Account, Tx, types, ReadOnlyFn } from "../../utils/deps.ts";

enum ErrCode {
ERR_PANIC = 1700,
ERR_VOTED_ALREADY,
ERR_NOTHING_STACKED,
ERR_USER_NOT_FOUND,
ERR_PROPOSAL_NOT_ACTIVE,
ERR_PROPOSAL_STILL_ACTIVE,
ERR_NO_CITY_ID,
ERR_VOTE_FAILED,
}

export class CCIP017ExtendDirectExecuteSunsetPeriod {
name = PROPOSALS.CCIP_017;
static readonly ErrCode = ErrCode;
chain: Chain;
deployer: Account;

constructor(chain: Chain, deployer: Account) {
this.chain = chain;
this.deployer = deployer;
}

// public functions

// execute() excluded since called by passProposal and CCD001

voteOnProposal(sender: Account, vote: boolean) {
return Tx.contractCall(this.name, "vote-on-proposal", [types.bool(vote)], sender.address);
}

// read-only functions

isExecutable() {
return this.callReadOnlyFn("is-executable");
}

isVoteActive() {
return this.callReadOnlyFn("is-vote-active");
}

getProposalInfo() {
return this.callReadOnlyFn("get-proposal-info");
}

getVotePeriod() {
return this.callReadOnlyFn("get-vote-period");
}

getVoteTotals() {
return this.callReadOnlyFn("get-vote-totals");
}

getVoterInfo(userId: number) {
return this.callReadOnlyFn("get-voter-info", [types.uint(userId)]);
}

getMiaVote(cityId: number, userId: number, scaled: boolean) {
return this.callReadOnlyFn("get-mia-vote", [types.uint(cityId), types.uint(userId), types.bool(scaled)]);
}

getNycVote(cityId: number, userId: number, scaled: boolean) {
return this.callReadOnlyFn("get-nyc-vote", [types.uint(cityId), types.uint(userId), types.bool(scaled)]);
}

// read-only function helper
private callReadOnlyFn(method: string, args: Array<any> = [], sender: Account = this.deployer): ReadOnlyFn {
const result = this.chain.callReadOnlyFn(this.name, method, args, sender?.address);
return result;
}
}
Loading

0 comments on commit 7dad7d6

Please sign in to comment.