Repository: public-awesome/launchpad
Pull request #504 (2023-02-02 02:40:35)

SG-268 Set minting limit to 3% of collection
Linear link: https://linear.app/stargaze/issue/SG-268/change-max-mint-limit-per-wallet

In this PR per the linear link: 
"Limited to 3% of collection size. 
If the collection size is less than 100, the max limit is 3 mint. 
Max of 50 tokens per wallet. 
Creators should be able to choose 1 per wallet. Below 100 max 3. Above 100 3%, rounding up"

In the code, in order to get 3% we use a fraction of Uint128(3) / Uint128)(100) and multiply it by the token count. Note that this can generate an error so Response<Result, ContractError> had to be used and bubbled up appropriately. Additionally, since we want to return a meaningful error message, we need to calculate 3% of the calculation when telling the user when the per address limit goes over 3% of the collection. 


Patch:
diff --git a/contracts/base-factory/schema/raw/execute.json b/contracts/base-factory/schema/raw/execute.json
new file mode 100644
index 00000000..5ead95b6
--- /dev/null
+++ b/contracts/base-factory/schema/raw/execute.json
@@ -0,0 +1,157 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "title": "ExecuteMsg",
+  "oneOf": [
+    {
+      "type": "object",
+      "required": [
+        "create_minter"
+      ],
+      "properties": {
+        "create_minter": {
+          "$ref": "#/definitions/CreateMinterMsg_for_Nullable_Empty"
+        }
+      },
+      "additionalProperties": false
+    }
+  ],
+  "definitions": {
+    "CollectionInfo_for_RoyaltyInfoResponse": {
+      "type": "object",
+      "required": [
+        "creator",
+        "description",
+        "image"
+      ],
+      "properties": {
+        "creator": {
+          "type": "string"
+        },
+        "description": {
+          "type": "string"
+        },
+        "explicit_content": {
+          "type": [
+            "boolean",
+            "null"
+          ]
+        },
+        "external_link": {
+          "type": [
+            "string",
+            "null"
+          ]
+        },
+        "image": {
+          "type": "string"
+        },
+        "royalty_info": {
+          "anyOf": [
+            {
+              "$ref": "#/definitions/RoyaltyInfoResponse"
+            },
+            {
+              "type": "null"
+            }
+          ]
+        },
+        "start_trading_time": {
+          "anyOf": [
+            {
+              "$ref": "#/definitions/Timestamp"
+            },
+            {
+              "type": "null"
+            }
+          ]
+        }
+      },
+      "additionalProperties": false
+    },
+    "CollectionParams": {
+      "type": "object",
+      "required": [
+        "code_id",
+        "info",
+        "name",
+        "symbol"
+      ],
+      "properties": {
+        "code_id": {
+          "description": "The collection code id",
+          "type": "integer",
+          "format": "uint64",
+          "minimum": 0.0
+        },
+        "info": {
+          "$ref": "#/definitions/CollectionInfo_for_RoyaltyInfoResponse"
+        },
+        "name": {
+          "type": "string"
+        },
+        "symbol": {
+          "type": "string"
+        }
+      },
+      "additionalProperties": false
+    },
+    "CreateMinterMsg_for_Nullable_Empty": {
+      "type": "object",
+      "required": [
+        "collection_params"
+      ],
+      "properties": {
+        "collection_params": {
+          "$ref": "#/definitions/CollectionParams"
+        },
+        "init_msg": {
+          "anyOf": [
+            {
+              "$ref": "#/definitions/Empty"
+            },
+            {
+              "type": "null"
+            }
+          ]
+        }
+      },
+      "additionalProperties": false
+    },
+    "Decimal": {
+      "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)",
+      "type": "string"
+    },
+    "Empty": {
+      "description": "An empty struct that serves as a placeholder in different places, such as contracts that don't set a custom message.\n\nIt is designed to be expressable in correct JSON and JSON Schema but contains no meaningful data. Previously we used enums without cases, but those cannot represented as valid JSON Schema (https://github.com/CosmWasm/cosmwasm/issues/451)",
+      "type": "object"
+    },
+    "RoyaltyInfoResponse": {
+      "type": "object",
+      "required": [
+        "payment_address",
+        "share"
+      ],
+      "properties": {
+        "payment_address": {
+          "type": "string"
+        },
+        "share": {
+          "$ref": "#/definitions/Decimal"
+        }
+      },
+      "additionalProperties": false
+    },
+    "Timestamp": {
+      "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```",
+      "allOf": [
+        {
+          "$ref": "#/definitions/Uint64"
+        }
+      ]
+    },
+    "Uint64": {
+      "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```",
+      "type": "string"
+    }
+  }
+}
diff --git a/contracts/base-factory/schema/raw/instantiate.json b/contracts/base-factory/schema/raw/instantiate.json
new file mode 100644
index 00000000..94887a80
--- /dev/null
+++ b/contracts/base-factory/schema/raw/instantiate.json
@@ -0,0 +1,85 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "title": "InstantiateMsg",
+  "type": "object",
+  "required": [
+    "params"
+  ],
+  "properties": {
+    "params": {
+      "$ref": "#/definitions/MinterParams_for_Nullable_Empty"
+    }
+  },
+  "additionalProperties": false,
+  "definitions": {
+    "Coin": {
+      "type": "object",
+      "required": [
+        "amount",
+        "denom"
+      ],
+      "properties": {
+        "amount": {
+          "$ref": "#/definitions/Uint128"
+        },
+        "denom": {
+          "type": "string"
+        }
+      }
+    },
+    "Empty": {
+      "description": "An empty struct that serves as a placeholder in different places, such as contracts that don't set a custom message.\n\nIt is designed to be expressable in correct JSON and JSON Schema but contains no meaningful data. Previously we used enums without cases, but those cannot represented as valid JSON Schema (https://github.com/CosmWasm/cosmwasm/issues/451)",
+      "type": "object"
+    },
+    "MinterParams_for_Nullable_Empty": {
+      "description": "Common params for all minters used for storage",
+      "type": "object",
+      "required": [
+        "code_id",
+        "creation_fee",
+        "max_trading_offset_secs",
+        "min_mint_price",
+        "mint_fee_bps"
+      ],
+      "properties": {
+        "code_id": {
+          "description": "The minter code id",
+          "type": "integer",
+          "format": "uint64",
+          "minimum": 0.0
+        },
+        "creation_fee": {
+          "$ref": "#/definitions/Coin"
+        },
+        "extension": {
+          "anyOf": [
+            {
+              "$ref": "#/definitions/Empty"
+            },
+            {
+              "type": "null"
+            }
+          ]
+        },
+        "max_trading_offset_secs": {
+          "type": "integer",
+          "format": "uint64",
+          "minimum": 0.0
+        },
+        "min_mint_price": {
+          "$ref": "#/definitions/Coin"
+        },
+        "mint_fee_bps": {
+          "type": "integer",
+          "format": "uint64",
+          "minimum": 0.0
+        }
+      },
+      "additionalProperties": false
+    },
+    "Uint128": {
+      "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```",
+      "type": "string"
+    }
+  }
+}
diff --git a/contracts/base-minter/schema/raw/execute.json b/contracts/base-minter/schema/raw/execute.json
new file mode 100644
index 00000000..1f88d795
--- /dev/null
+++ b/contracts/base-minter/schema/raw/execute.json
@@ -0,0 +1,60 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "title": "ExecuteMsg",
+  "oneOf": [
+    {
+      "type": "object",
+      "required": [
+        "mint"
+      ],
+      "properties": {
+        "mint": {
+          "type": "object",
+          "required": [
+            "token_uri"
+          ],
+          "properties": {
+            "token_uri": {
+              "type": "string"
+            }
+          },
+          "additionalProperties": false
+        }
+      },
+      "additionalProperties": false
+    },
+    {
+      "type": "object",
+      "required": [
+        "update_start_trading_time"
+      ],
+      "properties": {
+        "update_start_trading_time": {
+          "anyOf": [
+            {
+              "$ref": "#/definitions/Timestamp"
+            },
+            {
+              "type": "null"
+            }
+          ]
+        }
+      },
+      "additionalProperties": false
+    }
+  ],
+  "definitions": {
+    "Timestamp": {
+      "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```",
+      "allOf": [
+        {
+          "$ref": "#/definitions/Uint64"
+        }
+      ]
+    },
+    "Uint64": {
+      "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```",
+      "type": "string"
+    }
+  }
+}
diff --git a/contracts/base-minter/schema/raw/instantiate.json b/contracts/base-minter/schema/raw/instantiate.json
new file mode 100644
index 00000000..97ccd57f
--- /dev/null
+++ b/contracts/base-minter/schema/raw/instantiate.json
@@ -0,0 +1,222 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "title": "InstantiateMsg",
+  "type": "object",
+  "required": [
+    "create_msg",
+    "params"
+  ],
+  "properties": {
+    "create_msg": {
+      "$ref": "#/definitions/CreateMinterMsg_for_Nullable_Empty"
+    },
+    "params": {
+      "$ref": "#/definitions/MinterParams_for_Nullable_Empty"
+    }
+  },
+  "additionalProperties": false,
+  "definitions": {
+    "Coin": {
+      "type": "object",
+      "required": [
+        "amount",
+        "denom"
+      ],
+      "properties": {
+        "amount": {
+          "$ref": "#/definitions/Uint128"
+        },
+        "denom": {
+          "type": "string"
+        }
+      }
+    },
+    "CollectionInfo_for_RoyaltyInfoResponse": {
+      "type": "object",
+      "required": [
+        "creator",
+        "description",
+        "image"
+      ],
+      "properties": {
+        "creator": {
+          "type": "string"
+        },
+        "description": {
+          "type": "string"
+        },
+        "explicit_content": {
+          "type": [
+            "boolean",
+            "null"
+          ]
+        },
+        "external_link": {
+          "type": [
+            "string",
+            "null"
+          ]
+        },
+        "image": {
+          "type": "string"
+        },
+        "royalty_info": {
+          "anyOf": [
+            {
+              "$ref": "#/definitions/RoyaltyInfoResponse"
+            },
+            {
+              "type": "null"
+            }
+          ]
+        },
+        "start_trading_time": {
+          "anyOf": [
+            {
+              "$ref": "#/definitions/Timestamp"
+            },
+            {
+              "type": "null"
+            }
+          ]
+        }
+      },
+      "additionalProperties": false
+    },
+    "CollectionParams": {
+      "type": "object",
+      "required": [
+        "code_id",
+        "info",
+        "name",
+        "symbol"
+      ],
+      "properties": {
+        "code_id": {
+          "description": "The collection code id",
+          "type": "integer",
+          "format": "uint64",
+          "minimum": 0.0
+        },
+        "info": {
+          "$ref": "#/definitions/CollectionInfo_for_RoyaltyInfoResponse"
+        },
+        "name": {
+          "type": "string"
+        },
+        "symbol": {
+          "type": "string"
+        }
+      },
+      "additionalProperties": false
+    },
+    "CreateMinterMsg_for_Nullable_Empty": {
+      "type": "object",
+      "required": [
+        "collection_params"
+      ],
+      "properties": {
+        "collection_params": {
+          "$ref": "#/definitions/CollectionParams"
+        },
+        "init_msg": {
+          "anyOf": [
+            {
+              "$ref": "#/definitions/Empty"
+            },
+            {
+              "type": "null"
+            }
+          ]
+        }
+      },
+      "additionalProperties": false
+    },
+    "Decimal": {
+      "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)",
+      "type": "string"
+    },
+    "Empty": {
+      "description": "An empty struct that serves as a placeholder in different places, such as contracts that don't set a custom message.\n\nIt is designed to be expressable in correct JSON and JSON Schema but contains no meaningful data. Previously we used enums without cases, but those cannot represented as valid JSON Schema (https://github.com/CosmWasm/cosmwasm/issues/451)",
+      "type": "object"
+    },
+    "MinterParams_for_Nullable_Empty": {
+      "description": "Common params for all minters used for storage",
+      "type": "object",
+      "required": [
+        "code_id",
+        "creation_fee",
+        "max_trading_offset_secs",
+        "min_mint_price",
+        "mint_fee_bps"
+      ],
+      "properties": {
+        "code_id": {
+          "description": "The minter code id",
+          "type": "integer",
+          "format": "uint64",
+          "minimum": 0.0
+        },
+        "creation_fee": {
+          "$ref": "#/definitions/Coin"
+        },
+        "extension": {
+          "anyOf": [
+            {
+              "$ref": "#/definitions/Empty"
+            },
+            {
+              "type": "null"
+            }
+          ]
+        },
+        "max_trading_offset_secs": {
+          "type": "integer",
+          "format": "uint64",
+          "minimum": 0.0
+        },
+        "min_mint_price": {
+          "$ref": "#/definitions/Coin"
+        },
+        "mint_fee_bps": {
+          "type": "integer",
+          "format": "uint64",
+          "minimum": 0.0
+        }
+      },
+      "additionalProperties": false
+    },
+    "RoyaltyInfoResponse": {
+      "type": "object",
+      "required": [
+        "payment_address",
+        "share"
+      ],
+      "properties": {
+        "payment_address": {
+          "type": "string"
+        },
+        "share": {
+          "$ref": "#/definitions/Decimal"
+        }
+      },
+      "additionalProperties": false
+    },
+    "Timestamp": {
+      "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```",
+      "allOf": [
+        {
+          "$ref": "#/definitions/Uint64"
+        }
+      ]
+    },
+    "Uint128": {
+      "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```",
+      "type": "string"
+    },
+    "Uint64": {
+      "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```",
+      "type": "string"
+    }
+  }
+}
diff --git a/contracts/vending-minter/src/contract.rs b/contracts/vending-minter/src/contract.rs
index 9df6b0b3..7bf13218 100644
--- a/contracts/vending-minter/src/contract.rs
+++ b/contracts/vending-minter/src/contract.rs
@@ -9,6 +9,7 @@ use crate::state::{
     Config, ConfigExtension, CONFIG, MINTABLE_NUM_TOKENS, MINTABLE_TOKEN_POSITIONS, MINTER_ADDRS,
     SG721_ADDRESS, STATUS,
 };
+use crate::validation::{check_dynamic_per_address_limit, get_three_percent_of_tokens};
 #[cfg(not(feature = "library"))]
 use cosmwasm_std::entry_point;
 use cosmwasm_std::{
@@ -17,6 +18,7 @@ use cosmwasm_std::{
 };
 use cw2::set_contract_version;
 use cw721_base::{Extension, MintMsg};
+
 use cw_utils::{may_pay, maybe_addr, nonpayable, parse_reply_instantiate_data};
 use rand_core::{RngCore, SeedableRng};
 use rand_xoshiro::Xoshiro128PlusPlus;
@@ -74,9 +76,13 @@ pub fn instantiate(
         msg.init_msg.per_address_limit,
         msg.init_msg.num_tokens,
         factory_params.extension.max_per_address_limit,
-    ) {
+    )? {
         return Err(ContractError::InvalidPerAddressLimit {
-            max: msg.init_msg.num_tokens / 100,
+            max: display_max_mintable_tokens(
+                msg.init_msg.per_address_limit,
+                msg.init_msg.num_tokens,
+                factory_params.extension.max_per_address_limit,
+            )?,
             min: 1,
             got: msg.init_msg.per_address_limit,
         });
@@ -830,9 +836,13 @@ pub fn execute_update_per_address_limit(
         per_address_limit,
         config.extension.num_tokens,
         factory_params.extension.max_per_address_limit,
-    ) {
+    )? {
         return Err(ContractError::InvalidPerAddressLimit {
-            max: config.extension.num_tokens / 100,
+            max: display_max_mintable_tokens(
+                per_address_limit,
+                config.extension.num_tokens,
+                factory_params.extension.max_per_address_limit,
+            )?,
             min: 1,
             got: per_address_limit,
         });
@@ -924,16 +934,19 @@ fn mint_count(deps: Deps, info: &MessageInfo) -> Result<u32, StdError> {
     Ok(mint_count)
 }
 
-// Check per address limit to make sure it's <= 1% num tokens
-fn check_dynamic_per_address_limit(
+pub fn display_max_mintable_tokens(
     per_address_limit: u32,
     num_tokens: u32,
     max_per_address_limit: u32,
-) -> bool {
-    // If collection is small or large, no need to check per address limit
-    // ex: per_address_limit 50, no need to check if num_tokens > 5000
-    !((num_tokens > 100 && num_tokens < 100 * max_per_address_limit)
-        && per_address_limit > (num_tokens / 100))
+) -> Result<u32, ContractError> {
+    if per_address_limit > max_per_address_limit {
+        return Ok(max_per_address_limit);
+    }
+    if num_tokens < 100 {
+        return Ok(3_u32);
+    }
+    let three_percent = get_three_percent_of_tokens(num_tokens)?.u128();
+    Ok(three_percent as u32)
 }
 
 #[cfg_attr(not(feature = "library"), entry_point)]
diff --git a/contracts/vending-minter/src/error.rs b/contracts/vending-minter/src/error.rs
index f15636e4..4bf03919 100644
--- a/contracts/vending-minter/src/error.rs
+++ b/contracts/vending-minter/src/error.rs
@@ -3,7 +3,6 @@ use cw_utils::PaymentError;
 use sg1::FeeError;
 use thiserror::Error;
 use url::ParseError;
-
 #[derive(Error, Debug, PartialEq)]
 pub enum ContractError {
     #[error("{0}")]
@@ -98,4 +97,7 @@ pub enum ContractError {
 
     #[error("NoEnvTransactionIndex")]
     NoEnvTransactionIndex {},
+
+    #[error("Multiply Fraction Error")]
+    CheckedMultiplyFractionError {},
 }
diff --git a/contracts/vending-minter/src/lib.rs b/contracts/vending-minter/src/lib.rs
index ccc96a84..e061fa43 100644
--- a/contracts/vending-minter/src/lib.rs
+++ b/contracts/vending-minter/src/lib.rs
@@ -6,3 +6,4 @@ pub mod state;
 pub use crate::error::ContractError;
 
 pub mod helpers;
+pub mod validation;
diff --git a/contracts/vending-minter/src/validation.rs b/contracts/vending-minter/src/validation.rs
new file mode 100644
index 00000000..a3eb1ace
--- /dev/null
+++ b/contracts/vending-minter/src/validation.rs
@@ -0,0 +1,28 @@
+use crate::ContractError;
+use crate::ContractError::CheckedMultiplyFractionError;
+use cosmwasm_std::Uint128;
+
+pub fn get_three_percent_of_tokens(num_tokens: u32) -> Result<Uint128, ContractError> {
+    let three_percent = (Uint128::new(3), Uint128::new(100));
+    let three_percent_tokens = Uint128::from(num_tokens)
+        .checked_mul_ceil(three_percent)
+        .map_err(|_| CheckedMultiplyFractionError {})?;
+    Ok(three_percent_tokens)
+}
+
+// Check per address limit to make sure it's <= 1% num tokens
+pub fn check_dynamic_per_address_limit(
+    per_address_limit: u32,
+    num_tokens: u32,
+    max_per_address_limit: u32,
+) -> Result<bool, ContractError> {
+    if per_address_limit > max_per_address_limit {
+        return Ok(false);
+    }
+    if num_tokens < 100 {
+        return Ok(per_address_limit <= 3);
+    }
+    let three_percent_tokens = get_three_percent_of_tokens(num_tokens)?;
+    let result = Uint128::from(per_address_limit) <= three_percent_tokens;
+    Ok(result)
+}


Test patch:
diff --git a/test-suite/src/common_setup/setup_minter/vending_minter/mock_params.rs b/test-suite/src/common_setup/setup_minter/vending_minter/mock_params.rs
index 693fbf6b..a185dbd7 100644
--- a/test-suite/src/common_setup/setup_minter/vending_minter/mock_params.rs
+++ b/test-suite/src/common_setup/setup_minter/vending_minter/mock_params.rs
@@ -21,7 +21,7 @@ pub fn mock_init_extension(
         start_time: start_time.unwrap_or(Timestamp::from_nanos(GENESIS_MINT_START_TIME)),
         num_tokens: 100,
         mint_price: coin(MIN_MINT_PRICE, NATIVE_DENOM),
-        per_address_limit: 5,
+        per_address_limit: 3,
         whitelist: None,
     }
 }
diff --git a/test-suite/src/common_setup/templates.rs b/test-suite/src/common_setup/templates.rs
index fb384d8e..965c0232 100644
--- a/test-suite/src/common_setup/templates.rs
+++ b/test-suite/src/common_setup/templates.rs
@@ -9,13 +9,15 @@ use crate::common_setup::{
 
 use crate::common_setup::setup_minter::base_minter::setup::base_minter_sg721_nt_code_ids;
 use crate::common_setup::setup_minter::base_minter::setup::configure_base_minter;
-use cosmwasm_std::Timestamp;
+use cosmwasm_std::{coin, Timestamp};
 use sg2::tests::mock_collection_params_1;
 use sg_multi_test::StargazeApp;
-use sg_std::GENESIS_MINT_START_TIME;
+use sg_std::{GENESIS_MINT_START_TIME, NATIVE_DENOM};
 
 use super::msg::VendingAccounts;
 use super::setup_minter::base_minter::setup::base_minter_sg721_collection_code_ids;
+use super::setup_minter::common::constants::MIN_MINT_PRICE;
+use super::setup_minter::common::minter_params::minter_params_all;
 
 pub fn vending_minter_template(num_tokens: u32) -> VendingTemplateResponse<VendingAccounts> {
     let mut app = custom_mock_app();
@@ -38,6 +40,40 @@ pub fn vending_minter_template(num_tokens: u32) -> VendingTemplateResponse<Vendi
     }
 }
 
+pub fn vending_minter_per_address_limit(
+    num_tokens: u32,
+    per_address_limit: u32,
+) -> VendingTemplateResponse<VendingAccounts> {
+    let mut app = custom_mock_app();
+    let (creator, buyer) = setup_accounts(&mut app);
+    let start_time = Timestamp::from_nanos(GENESIS_MINT_START_TIME);
+    let collection_params = mock_collection_params_1(Some(start_time));
+    let init_msg = vending_factory::msg::VendingMinterInitMsgExtension {
+        base_token_uri: "ipfs://aldkfjads".to_string(),
+        payment_address: None,
+        start_time: Timestamp::from_nanos(GENESIS_MINT_START_TIME),
+        num_tokens,
+        mint_price: coin(MIN_MINT_PRICE, NATIVE_DENOM),
+        per_address_limit,
+        whitelist: Some("invalid address".to_string()),
+    };
+
+    let minter_params = minter_params_all(num_tokens, None, None, Some(init_msg));
+    let code_ids = vending_minter_code_ids(&mut app);
+    let minter_collection_response: Vec<MinterCollectionResponse> = configure_minter(
+        &mut app,
+        creator.clone(),
+        vec![collection_params],
+        vec![minter_params],
+        code_ids,
+    );
+    VendingTemplateResponse {
+        router: app,
+        collection_response_vec: minter_collection_response,
+        accts: VendingAccounts { creator, buyer },
+    }
+}
+
 pub fn vending_minter_with_start_time(
     num_tokens: u32,
     start_time: Timestamp,
diff --git a/test-suite/src/vending_minter/tests/address_limit.rs b/test-suite/src/vending_minter/tests/address_limit.rs
index d0d20ac0..8472edf0 100644
--- a/test-suite/src/vending_minter/tests/address_limit.rs
+++ b/test-suite/src/vending_minter/tests/address_limit.rs
@@ -1,7 +1,10 @@
 use crate::common_setup::{
+    contract_boxes::custom_mock_app,
     msg::MinterCollectionResponse,
+    setup_accounts_and_block::setup_accounts,
     setup_minter::common::minter_params::minter_params_token,
     setup_minter::vending_minter::setup::{configure_minter, vending_minter_code_ids},
+    templates::vending_minter_per_address_limit,
 };
 use crate::common_setup::{
     setup_accounts_and_block::{coins_for_msg, setup_block_time},
@@ -93,26 +96,165 @@ fn check_per_address_limit() {
 }
 
 #[test]
-fn check_dynamic_per_address_limit() {
-    // let mut router = custom_mock_app();
-    let num_tokens = 400;
-    let vt = vending_minter_template(num_tokens);
-    let (mut router, creator) = (vt.router, vt.accts.creator);
-    let err = vt.collection_response_vec[0].error.as_ref();
+fn check_per_address_limit_3_percent() {
+    let vt = vending_minter_template(200);
+    let (mut router, creator, buyer) = (vt.router, vt.accts.creator, vt.accts.buyer);
+    let minter_addr = vt.collection_response_vec[0].minter.clone().unwrap();
+    // Set to genesis mint start time
+    setup_block_time(&mut router, GENESIS_MINT_START_TIME, None);
 
-    setup_block_time(&mut router, GENESIS_MINT_START_TIME - 1, None);
+    // 3% of 200 is 6, cant have 7 per address limit
+    let per_address_limit_msg = ExecuteMsg::UpdatePerAddressLimit {
+        per_address_limit: 7,
+    };
+    let res = router.execute_contract(
+        creator.clone(),
+        minter_addr.clone(),
+        &per_address_limit_msg,
+        &[],
+    );
+    assert!(res.is_err());
 
-    assert_eq!(
-        err.unwrap().source().unwrap().source().unwrap().to_string(),
-        ContractError::InvalidPerAddressLimit {
-            max: num_tokens / 100,
-            min: 1,
-            got: 5
-        }
-        .to_string()
+    // can set to 3% which is 6
+    let per_address_limit_msg = ExecuteMsg::UpdatePerAddressLimit {
+        per_address_limit: 6,
+    };
+    let res = router.execute_contract(creator, minter_addr.clone(), &per_address_limit_msg, &[]);
+    assert!(res.is_ok());
+
+    for _ in 0..6 {
+        let mint_msg = ExecuteMsg::Mint {};
+        let res = router.execute_contract(
+            buyer.clone(),
+            minter_addr.clone(),
+            &mint_msg,
+            &coins(MINT_PRICE, NATIVE_DENOM),
+        );
+        assert!(res.is_ok());
+    }
+
+    // Second mint fails from exceeding per address limit
+    let mint_msg = ExecuteMsg::Mint {};
+    let res = router.execute_contract(
+        buyer,
+        minter_addr,
+        &mint_msg,
+        &coins(MINT_PRICE, NATIVE_DENOM),
     );
+    assert!(res.is_err());
+}
+
+#[test]
+fn check_allow_3_mint_when_less_than_100_size_collection() {
+    let vt = vending_minter_template(25);
+    let (mut router, creator, _) = (vt.router, vt.accts.creator, vt.accts.buyer);
+    let minter_addr = vt.collection_response_vec[0].minter.clone().unwrap();
+    // Set to genesis mint start time
+    setup_block_time(&mut router, GENESIS_MINT_START_TIME, None);
+
+    // can't set to 4 according to per_address_limit <= 3 check
+    let per_address_limit_msg = ExecuteMsg::UpdatePerAddressLimit {
+        per_address_limit: 4,
+    };
+    let res = router.execute_contract(
+        creator.clone(),
+        minter_addr.clone(),
+        &per_address_limit_msg,
+        &[],
+    );
+    assert!(res.is_err());
+
+    // can set to 3 which is above 3% but passes per_address_limit <= 3  check
+    let per_address_limit_msg = ExecuteMsg::UpdatePerAddressLimit {
+        per_address_limit: 3,
+    };
+    let res = router.execute_contract(creator, minter_addr, &per_address_limit_msg, &[]);
+    assert!(res.is_ok());
+}
+
+#[test]
+fn check_3_percent_round_up() {
+    let vt = vending_minter_template(215);
+    let (mut router, creator, buyer) = (vt.router, vt.accts.creator, vt.accts.buyer);
+    let minter_addr = vt.collection_response_vec[0].minter.clone().unwrap();
+    // Set to genesis mint start time
+    setup_block_time(&mut router, GENESIS_MINT_START_TIME, None);
 
-    let num_tokens = 1000;
+    // 3% of 215 is 6.45, we should round up to 7 allowed, 8 too many
+    let per_address_limit_msg = ExecuteMsg::UpdatePerAddressLimit {
+        per_address_limit: 8,
+    };
+    let res = router.execute_contract(
+        creator.clone(),
+        minter_addr.clone(),
+        &per_address_limit_msg,
+        &[],
+    );
+    assert!(res.is_err());
+
+    // can set to 3% which is 7
+    let per_address_limit_msg = ExecuteMsg::UpdatePerAddressLimit {
+        per_address_limit: 7,
+    };
+    let res = router.execute_contract(creator, minter_addr.clone(), &per_address_limit_msg, &[]);
+    assert!(res.is_ok());
+
+    for _ in 0..7 {
+        let mint_msg = ExecuteMsg::Mint {};
+        let res = router.execute_contract(
+            buyer.clone(),
+            minter_addr.clone(),
+            &mint_msg,
+            &coins(MINT_PRICE, NATIVE_DENOM),
+        );
+        assert!(res.is_ok());
+    }
+
+    // Second mint fails from exceeding per address limit
+    let mint_msg = ExecuteMsg::Mint {};
+    let res = router.execute_contract(
+        buyer,
+        minter_addr,
+        &mint_msg,
+        &coins(MINT_PRICE, NATIVE_DENOM),
+    );
+    assert!(res.is_err());
+}
+
+#[test]
+fn check_per_address_limit_above_max_per_address_limit() {
+    let vt = vending_minter_template(5000);
+    let (mut router, creator, _) = (vt.router, vt.accts.creator, vt.accts.buyer);
+    let minter_addr = vt.collection_response_vec[0].minter.clone().unwrap();
+    // Set to genesis mint start time
+    setup_block_time(&mut router, GENESIS_MINT_START_TIME, None);
+
+    // 3% of 5000 is 150, but we will get blocked by the max per address limit flag
+    let per_address_limit_msg = ExecuteMsg::UpdatePerAddressLimit {
+        per_address_limit: 51,
+    };
+    let res = router.execute_contract(
+        creator.clone(),
+        minter_addr.clone(),
+        &per_address_limit_msg,
+        &[],
+    );
+    assert!(res.is_err());
+
+    // can set to 50
+    let per_address_limit_msg = ExecuteMsg::UpdatePerAddressLimit {
+        per_address_limit: 50,
+    };
+    let res = router.execute_contract(creator, minter_addr, &per_address_limit_msg, &[]);
+    assert!(res.is_ok());
+}
+
+#[test]
+fn check_invalid_update_per_address_limit() {
+    let max_per_address_limit = 50;
+    let mut router = custom_mock_app();
+    let (creator, _) = setup_accounts(&mut router);
+    let num_tokens = 300;
     let start_time = Timestamp::from_nanos(GENESIS_MINT_START_TIME);
     let collection_params = mock_collection_params_1(Some(start_time));
     let minter_params = minter_params_token(num_tokens);
@@ -129,7 +271,7 @@ fn check_dynamic_per_address_limit() {
 
     let minter_addr = minter_collection_response[0].minter.clone().unwrap();
     let update_msg = ExecuteMsg::UpdatePerAddressLimit {
-        per_address_limit: 11,
+        per_address_limit: 110,
     };
     let err = router
         .execute_contract(creator, minter_addr, &update_msg, &[])
@@ -137,9 +279,29 @@ fn check_dynamic_per_address_limit() {
     assert_eq!(
         err.source().unwrap().to_string(),
         ContractError::InvalidPerAddressLimit {
-            max: num_tokens / 100,
+            max: max_per_address_limit,
+            min: 1,
+            got: 110,
+        }
+        .to_string()
+    );
+}
+
+#[test]
+fn check_invalid_instantiate_per_address_limit() {
+    let num_tokens = 400;
+    let per_address_limit = 30;
+    let vt = vending_minter_per_address_limit(num_tokens, per_address_limit);
+    let (mut router, _) = (vt.router, vt.accts.creator);
+    let err = vt.collection_response_vec[0].error.as_ref();
+
+    setup_block_time(&mut router, GENESIS_MINT_START_TIME - 1, None);
+    assert_eq!(
+        err.unwrap().source().unwrap().source().unwrap().to_string(),
+        ContractError::InvalidPerAddressLimit {
+            max: 12,
             min: 1,
-            got: 11,
+            got: 30
         }
         .to_string()
     );
diff --git a/test-suite/src/vending_minter/tests/whitelist.rs b/test-suite/src/vending_minter/tests/whitelist.rs
index 37438d95..837f7a93 100644
--- a/test-suite/src/vending_minter/tests/whitelist.rs
+++ b/test-suite/src/vending_minter/tests/whitelist.rs
@@ -47,7 +47,7 @@ fn invalid_whitelist_instantiate() {
         start_time: Timestamp::from_nanos(GENESIS_MINT_START_TIME),
         num_tokens: 100,
         mint_price: coin(MIN_MINT_PRICE, NATIVE_DENOM),
-        per_address_limit: 5,
+        per_address_limit: 3,
         whitelist: Some("invalid address".to_string()),
     };