Skip to content

Commit

Permalink
add vesting guide
Browse files Browse the repository at this point in the history
  • Loading branch information
jinglescode committed Aug 31, 2024
1 parent 9d2c932 commit 4fe5053
Show file tree
Hide file tree
Showing 7 changed files with 311 additions and 4 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/playground/public/guides/vesting.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions apps/playground/src/data/links-guides.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,13 @@ export const guidestandalone = {
thumbnail: "/guides/standalone.png",
image: "/guides/salt-harvesting-3060093_1280.jpg",
};
export const guideVesting = {
title: "Vesting Script End-to-End",
desc: "Learn how to vesting contract that locks up funds for a period of time and allows the owner to withdraw the funds after the lockup period.",
link: "/guides/vesting",
thumbnail: "/guides/vesting.png",
image: "/guides/laptop-3196481_640.jpg",
};

export const linksGuides: MenuItem[] = [
guidenextjs,
Expand All @@ -68,6 +75,7 @@ export const linksGuides: MenuItem[] = [
guidetransactions,
guideaiken,
guidestandalone,
guideVesting,
];

export const metaGuides: MenuItem = {
Expand Down
12 changes: 12 additions & 0 deletions apps/playground/src/pages/guides/vesting/demo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { VestingDepositFundDemo } from "../../smart-contracts/vesting/deposit-fund";
import { VestingWithdrawFundDemo } from "../../smart-contracts/vesting/withdraw-fund";

export default function Demo() {
return (
<>
<h2>Demo</h2>
<VestingDepositFundDemo />
<VestingWithdrawFundDemo />
</>
);
}
287 changes: 287 additions & 0 deletions apps/playground/src/pages/guides/vesting/index.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,287 @@
import LayoutImageHeaderAndBody from "~/components/layouts/image-header-and-body";
import { guideVesting } from "~/data/links-guides";
import Demo from "./demo";

export default function MDXPage({ children }) {
return (
<LayoutImageHeaderAndBody
title={guideVesting.title}
description={guideVesting.description}
image={guideVesting.image}
cover={guideVesting.cover}
>
<>
{children}
<Demo />
</>
</LayoutImageHeaderAndBody>
);
}

Vesting contract is a smart contract that locks up funds for a period of time and allows the owner to withdraw the funds after the lockup period. Usually, vesting contract defines a beneficiary who can be different from the original owner.

When a new employee joins an organization, they typically receive a promise of compensation to be disbursed after a specified duration of employment. This arrangement often involves the organization depositing the funds into a vesting contract, with the employee gaining access to the funds upon the completion of a predetermined lockup period. Through the utilization of vesting contracts, organizations establish a mechanism to encourage employee retention by linking financial rewards to tenure.

## On-Chain code

First, we define the datum's shape, as this datum serves as configuration and contains the different parameters of our vesting operation.

```rs
pub type VestingDatum {
/// POSIX time in milliseconds, e.g. 1672843961000
lock_until: Int,
/// Owner's credentials
owner: ByteArray,
/// Beneficiary's credentials
beneficiary: ByteArray,
}
```

In this example, we define a `VestingDatum` that contains the following fields:

- `lock_until`: The POSIX timestamp in milliseconds until which the funds are locked.
- `owner`: The credentials (public key hash) of the owner of the funds.
- `beneficiary`: The credentials (public key hash) of the beneficiary of the funds.

This datum can be found in `aiken-vesting/aiken-workspace/lib/vesting/types.ak`.

Next, we define the spend validator.

```rs
use aiken/transaction.{ScriptContext, Spend}
use vesting/types.{VestingDatum}
use vodka_extra_signatories.{key_signed}
use vodka_validity_range.{valid_after}

validator {
pub fn vesting(datum: VestingDatum, _redeemer: Data, ctx: ScriptContext) {
// In principle, scripts can be used for different purpose (e.g. minting
// assets). Here we make sure it's only used when 'spending' from a eUTxO
when ctx.purpose is {
Spend(_) -> or {
key_signed(ctx.transaction.extra_signatories, datum.owner),
and {
key_signed(ctx.transaction.extra_signatories, datum.beneficiary),
valid_after(ctx.transaction.validity_range, datum.lock_until),
},
}
_ -> False
}
}
}
```

In this example, we define a `vesting` validator that ensures the following conditions are met:

- The transaction must be signed by owner

Or:

- The transaction must be signed by beneficiary
- The transaction must be valid after the lockup period

This validator can be found in `aiken-vesting/aiken-workspace/validators/vesting.ak`.

### How it works

The owner of the funds deposits the funds into the vesting contract. The funds are locked up until the lockup period expires.

Transactions can include validity intervals that specify when the transaction is valid, both from and until a certain time. The ledger verifies these validity bounds before executing a script and will only proceed if they are legitimate.

This approach allows scripts to incorporate a sense of time while maintaining determinism within the script's context. For instance, if a transaction has a lower bound `A`, we can infer that the current time is at least `A`.

It's important to note that since we don't control the upper bound, a transaction might be executed even 30 years after the vesting delay. However, from the script's perspective, this is entirely acceptable.

The beneficiary can withdraw the funds after the lockup period expires. The beneficiary can also be different from the owner of the funds.

## Testing

To test the vesting contract, we have provided the a comphrehensive test script,you can run tests with `aiken check`.

The test script includes the following test cases:

- success unlocking
- success unlocking with only owner signature
- success unlocking with beneficiary signature and time passed
- fail unlocking with only beneficiary signature
- fail unlocking with only time passed

We recommend you to check out `aiken-vesting/aiken-workspace/validators/tests/vesting.ak` to learn more.

## Compile and build script

To compile the script, run the following command:

```sh
aiken build
```

This command will generate a CIP-0057 Plutus blueprint, which you can find in `aiken-vesting/aiken-workspace/plutus.json`.

## Off-Chain code

### Deposit funds

First, the owner can deposit funds into the vesting contract. The owner can specify the lockup period and the beneficiary of the funds.

```ts
const assets: Asset[] = [
{
unit: "lovelace",
quantity: "10000000",
},
];

const lockUntilTimeStamp = new Date();
lockUntilTimeStamp.setMinutes(lockUntilTimeStamp.getMinutes() + 1);

const beneficiary =
"addr_test1qpvx0sacufuypa2k4sngk7q40zc5c4npl337uusdh64kv0uafhxhu32dys6pvn6wlw8dav6cmp4pmtv7cc3yel9uu0nq93swx9";
```

In this example, we deposit 10 ADA into the vesting contract. The funds are locked up for 1 minute, and the beneficiary is specified.

Then, we prepare a few variables to be used in the transaction. We get the wallet address and the UTXOs of the wallet. We also get the script address of the vesting contract, to send the funds to the script address. We also get the owner and beneficiary public key hashes.

```ts
const { utxos, walletAddress } = await getWalletInfoForTx();

const { scriptAddr } = getScript();

const { pubKeyHash: ownerPubKeyHash } = deserializeAddress(walletAddress);
const { pubKeyHash: beneficiaryPubKeyHash } = deserializeAddress(beneficiary);
```

Next, we construct the transaction to deposit the funds into the vesting contract.

```ts
const txBuilder = new MeshTxBuilder({
fetcher: blockchainProvider,
submitter: blockchainProvider,
});

await txBuilder
.txOut(scriptAddr, amount)
.txOutInlineDatumValue(
mConStr0([lockUntilTimeStampMs, ownerPubKeyHash, beneficiaryPubKeyHash])
)
.changeAddress(walletAddress)
.selectUtxosFrom(utxos)
.complete();

const unsignedTx = txBuilder.txHex;
```

In this example, we construct the transaction to deposit the funds into the vesting contract. We specify the script address of the vesting contract, the amount to deposit, and the lockup period, owner, and beneficiary of the funds.

Finally, we sign and submit the transaction.

```ts
const signedTx = await wallet.signTx(unsignedTx);
const txHash = await wallet.submitTx(signedTx);
```

To execute this code, ensure you have defined blockfrost key in the `.env` file. You can also define your wallet mnemonic in `aiken-vesting/src/configs.ts` file.

You can run the following command execute the deposit funds code:

```sh
npm run deposit
```

Upon successful execution, you will receive a transaction hash. Save this transaction hash for withdrawing the funds.

Example of a [successful deposit transaction](https://preprod.cardanoscan.io/transaction/ede9f8176fe41f0c84cfc9802b693dedb5500c0cbe4377b7bb0d57cf0435200b).

### Withdraw funds

After the lockup period expires, the beneficiary can withdraw the funds from the vesting contract. The owner can also withdraw the funds from the vesting contract.

First, let's look for the UTxOs containing the funds locked in the vesting contract.

```ts
const txHashFromDesposit =
"ede9f8176fe41f0c84cfc9802b693dedb5500c0cbe4377b7bb0d57cf0435200b";
const utxos = await blockchainProvider.fetchUTxOs(txHash);
const vestingUtxo = utxos[0];
```

In this example, we fetch the UTxOs containing the funds locked in the vesting contract. We specify the transaction hash of the deposit transaction.

Like before, we prepare a few variables to be used in the transaction. We get the wallet address and the UTXOs of the wallet. We also get the script address of the vesting contract, to send the funds to the script address. We also get the owner and beneficiary public key hashes.

```ts
const { utxos, walletAddress, collateral } = await getWalletInfoForTx();
const { input: collateralInput, output: collateralOutput } = collateral;

const { scriptAddr, scriptCbor } = getScript();
const { pubKeyHash } = deserializeAddress(walletAddress);
```

Next, we prepare the datum and the slot number to set the transaction valid interval to be valid only after the slot.

```ts
const datum = deserializeDatum<VestingDatum>(vestingUtxo.output.plutusData!);

const invalidBefore =
unixTimeToEnclosingSlot(
Math.min(datum.fields[0].int as number, Date.now() - 15000),
SLOT_CONFIG_NETWORK.preprod
) + 1;
```

In this example, we prepare the datum and the slot number to set the transaction valid interval to be valid only after the slot. We get the lockup period from the datum and set the transaction valid interval to be valid only after the lockup period.

Next, we construct the transaction to withdraw the funds from the vesting contract.

```ts
const txBuilder = new MeshTxBuilder({
fetcher: blockchainProvider,
submitter: blockchainProvider,
});

await txBuilder
.spendingPlutusScriptV2()
.txIn(
vestingUtxo.input.txHash,
vestingUtxo.input.outputIndex,
vestingUtxo.output.amount,
scriptAddr
)
.spendingReferenceTxInInlineDatumPresent()
.spendingReferenceTxInRedeemerValue("")
.txInScript(scriptCbor)
.txOut(walletAddress, [])
.txInCollateral(
collateralInput.txHash,
collateralInput.outputIndex,
collateralOutput.amount,
collateralOutput.address
)
.invalidBefore(invalidBefore)
.requiredSignerHash(pubKeyHash)
.changeAddress(walletAddress)
.selectUtxosFrom(utxos)
.complete();

const unsignedTx = txBuilder.txHex;
```

In this example, we construct the transaction to withdraw the funds from the vesting contract. We specify the UTxO containing the funds locked in the vesting contract, the script address of the vesting contract, the wallet address to send the funds to, and the transaction valid interval.

Finally, we sign and submit the transaction. Notice that since we are unlocking fund from validator, partial sign has to be specified by passing a `true` parameter into `wallet.signTx`.

```ts
const signedTx = await wallet.signTx(unsignedTx, true);
const txHash = await wallet.submitTx(signedTx);
```

To execute this code, update `aiken-vesting/src/withdraw-fund.ts` with the transaction hash from the deposit transaction. Ensure you have defined blockfrost key in the `.env` file. You can also define your wallet mnemonic in `aiken-vesting/src/configs.ts` file.

Run the following command:

```sh
npm run withdraw
```

Example of a [successful withdraw transaction](https://preprod.cardanoscan.io/transaction/b108f91a1dcd1b4c0bc978fb7557fc23ad052f1681cca078aa2515f8ab01e05e).
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export default function VestingDepositFund() {
sidebarTo="depositFund"
title="Deposit Fund"
leftSection={Left()}
rightSection={Right()}
rightSection={VestingDepositFundDemo()}
/>
);
}
Expand All @@ -41,7 +41,7 @@ function Left() {
);
}

function Right() {
export function VestingDepositFundDemo() {
const { wallet, connected } = useWallet();
const [userInput, setUserInput] = useState<string>("5000000");
const [userInput2, setUserInput2] = useState<string>(demoAddresses.testnet);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export default function VestingWithdrawFund() {
sidebarTo="withdrawFund"
title="Withdraw Fund"
leftSection={Left()}
rightSection={Right()}
rightSection={VestingWithdrawFundDemo()}
/>
);
}
Expand All @@ -39,7 +39,7 @@ function Left() {
);
}

function Right() {
export function VestingWithdrawFundDemo() {
const { wallet, connected } = useWallet();
const [userInput, setUserInput] = useState<string>("");

Expand Down

0 comments on commit 4fe5053

Please sign in to comment.